├── .circleci └── config.yml ├── .gitignore ├── Config └── redis.json ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── RedisProvider │ ├── Exports.swift │ ├── Provider.swift │ └── RedisCache.swift └── Tests ├── LinuxMain.swift └── RedisProviderTests ├── ProviderTests.swift ├── RedisCacheTests.swift └── Utilities.swift /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | macos: 5 | macos: 6 | xcode: "9.0" 7 | steps: 8 | - run: brew install redis vapor/tap/vapor 9 | - run: brew services start redis 10 | - checkout 11 | - run: swift build 12 | - run: swift test 13 | 14 | linux-3: 15 | docker: 16 | - image: swift:3.1.1 17 | - image: redis:3.2 18 | steps: 19 | - run: apt-get install -yq libssl-dev 20 | - checkout 21 | - run: swift build 22 | - run: swift test 23 | 24 | linux-4: 25 | docker: 26 | - image: swift:4.0.3 27 | - image: redis:3.2 28 | steps: 29 | - run: apt-get install -yq libssl-dev 30 | - checkout 31 | - run: swift build 32 | - run: swift test 33 | 34 | workflows: 35 | version: 2 36 | tests: 37 | jobs: 38 | - macos 39 | - linux-3 40 | - linux-4 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | *.rdb 6 | .swift-version 7 | Package.pins 8 | 9 | -------------------------------------------------------------------------------- /Config/redis.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "127.0.0.1", 3 | "port": 6379 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "RedisProvider", 5 | dependencies: [ 6 | // Pure-Swift Redis client implemented from the original protocol spec. 7 | .Package(url: "https://github.com/vapor/redis.git", majorVersion: 2), 8 | 9 | // Importing Vapor for access to the Provider protocol. 10 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2) 11 | ] 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Redis Provider 3 |
4 |
5 | 6 | Documentation 7 | 8 | 9 | Slack Team 10 | 11 | 12 | MIT License 13 | 14 | 15 | Continuous Integration 16 | 17 | 18 | Swift 3.1 19 | 20 |

-------------------------------------------------------------------------------- /Sources/RedisProvider/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import Redis 2 | -------------------------------------------------------------------------------- /Sources/RedisProvider/Provider.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Transport 3 | import URI 4 | 5 | /// Provides a RedisCache object to Vapor 6 | /// when added to a Droplet. 7 | public final class Provider: Vapor.Provider { 8 | public static let repositoryName = "redis-provider" 9 | 10 | public init(config: Config) throws { } 11 | 12 | public func boot(_ config: Config) throws { 13 | config.addConfigurable(cache: RedisCache.init, name: "redis") 14 | } 15 | 16 | public func boot(_ drop: Droplet) throws { } 17 | 18 | public func beforeRun(_: Droplet) throws { } 19 | } 20 | 21 | extension RedisCache: ConfigInitializable { 22 | public convenience init(config: Config) throws { 23 | guard let redis = config["redis"]?.object else { 24 | throw ConfigError.missingFile("redis") 25 | } 26 | 27 | if let url = redis["url"]?.string { 28 | let encoding = redis["encoding"]?.string 29 | try self.init(url: url, encoding: encoding) 30 | } else { 31 | guard let hostname = redis["hostname"]?.string else { 32 | throw ConfigError.missing( 33 | key: ["hostname"], 34 | file: "redis", 35 | desiredType: String.self 36 | ) 37 | } 38 | 39 | guard let port = redis["port"]?.int?.port else { 40 | throw ConfigError.missing( 41 | key: ["port"], 42 | file: "redis", 43 | desiredType: Port.self 44 | ) 45 | } 46 | 47 | let password = redis["password"]?.string 48 | let database = redis["database"]?.int 49 | 50 | try self.init( 51 | hostname: hostname, 52 | port: port, 53 | password: password, 54 | database: database 55 | ) 56 | } 57 | } 58 | 59 | //accepts a heroku redis connection string in the format of: 60 | //redis://user:password@hostname:port/database 61 | public convenience init(url: String, encoding: String?) throws { 62 | let uri = try URI(url) 63 | 64 | guard uri.scheme == "redis" else { 65 | throw ConfigError.missing(key: ["url"], file: "redis", desiredType: String.self) 66 | } 67 | 68 | let host = uri.hostname 69 | let password = uri.userInfo?.info 70 | guard let port = uri.port else { 71 | throw ConfigError.missing(key: ["url.port"], file: "redis", desiredType: Port.self) 72 | } 73 | 74 | let database = Int(uri.path.components(separatedBy: "/").last ?? "") 75 | 76 | try self.init( 77 | hostname: host, 78 | port: Port(port), 79 | password: password, 80 | database: database 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/RedisProvider/RedisCache.swift: -------------------------------------------------------------------------------- 1 | import Cache 2 | import Node 3 | import JSON 4 | import Core 5 | import Transport 6 | 7 | public struct RedisContext: Context {} 8 | 9 | /// Uses an underlying Redbird 10 | /// instance to conform to the CacheProtocol. 11 | public final class RedisCache: CacheProtocol { 12 | public typealias ClientFactory = () throws -> Redis.TCPClient 13 | public let makeClient: ClientFactory 14 | private let context: RedisContext 15 | 16 | /// Creates a RedisCache from the address, 17 | /// port, and password credentials. 18 | /// 19 | /// Password should be nil if not required. 20 | public convenience init(hostname: String, port: Port, password: String? = nil, database: Int? = nil) throws { 21 | self.init { 22 | let client = try TCPClient( 23 | hostname: hostname, 24 | port: port, 25 | password: password 26 | ) 27 | 28 | if let database = database { 29 | try client.command(try Command("select"), [database.description]) 30 | } 31 | 32 | return client 33 | } 34 | } 35 | 36 | /// Create a new RedisCache from a Redbird. 37 | public init(_ clientFactory: @escaping ClientFactory) { 38 | makeClient = clientFactory 39 | self.context = RedisContext() 40 | } 41 | 42 | /// Returns a key from the underlying Redbird 43 | /// instance by using the GET command. 44 | /// Try to deserialize else return original 45 | public func get(_ key: String) throws -> Node? { 46 | guard let bytes = try makeClient() 47 | .command(.get, [key])? 48 | .bytes 49 | else { 50 | return nil 51 | } 52 | 53 | do { 54 | return try JSON(bytes: bytes) 55 | .makeNode(in: context) 56 | } catch { 57 | return Node.bytes(bytes) 58 | } 59 | } 60 | 61 | /// Sets a key to the supplied value in the 62 | /// underlying Redbird instance using the 63 | /// SET command. 64 | /// Serializing Node if not a string 65 | public func set(_ key: String, _ value: Node, expiration: Date?) throws { 66 | let serialized: Bytes 67 | switch value.wrapped { 68 | case .string(let s): 69 | serialized = s.makeBytes() 70 | case .bytes(let b): 71 | serialized = b 72 | default: 73 | serialized = try JSON(value).serialize() 74 | } 75 | 76 | let key = key.makeBytes() 77 | let client = try makeClient() 78 | 79 | try client.command(.set, [key, serialized]) 80 | if let exp = expiration { 81 | let time = Int(exp.timeIntervalSinceNow).description.makeBytes() 82 | try client.command(Command("expire"), [key, time]) 83 | } 84 | } 85 | 86 | /// Deletes a key from the underlying Redbird 87 | /// instance using the DEL command. 88 | public func delete(_ key: String) throws { 89 | try makeClient().command(.delete, [key]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import RedisProviderTests 3 | 4 | XCTMain([ 5 | testCase(ProviderTests.allTests), 6 | testCase(RedisCacheTests.allTests), 7 | ]) 8 | -------------------------------------------------------------------------------- /Tests/RedisProviderTests/ProviderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Vapor 3 | @testable import RedisProvider 4 | import Cache 5 | 6 | class ProviderTests: XCTestCase { 7 | static let allTests = [ 8 | ("testConfig", testConfig), 9 | ("testConfigMissing", testConfigMissing), 10 | ("testNotConfigured", testNotConfigured), 11 | ("testDatabaseSelect", testDatabaseSelect) 12 | ] 13 | 14 | func testConfig() throws { 15 | var config = Config([:]) 16 | try config.set("droplet.cache", "redis") 17 | try config.set("redis.hostname", RedisCache.testAddress) 18 | try config.set("redis.port", RedisCache.testPort) 19 | try config.addProvider(RedisProvider.Provider.self) 20 | let drop = try Droplet(config: config) 21 | 22 | XCTAssert(drop.cache is RedisCache) 23 | } 24 | 25 | func testConfigMissing() throws { 26 | var config = Config([:]) 27 | try config.set("droplet.cache", "redis") 28 | try config.addProvider(RedisProvider.Provider.self) 29 | 30 | do { 31 | _ = try Droplet(config) 32 | XCTFail("Should have failed.") 33 | } catch ConfigError.missingFile(let file) { 34 | XCTAssertEqual(file, "redis") 35 | } catch { 36 | XCTFail("Wrong error: \(error)") 37 | } 38 | } 39 | 40 | func testNotConfigured() throws { 41 | var config = Config([:]) 42 | try config.set("droplet.cache", "memory") 43 | try config.addProvider(RedisProvider.Provider.self) 44 | 45 | let drop = try Droplet(config: config) 46 | 47 | XCTAssert(drop.cache is MemoryCache) 48 | } 49 | 50 | func testUrlConfig() throws { 51 | var config = Config([:]) 52 | try config.set("droplet.cache", "redis") 53 | try config.set("redis.url", "redis://:password@\(RedisCache.testAddress):\(RedisCache.testPort)/2") 54 | try config.addProvider(RedisProvider.Provider.self) 55 | 56 | let drop = try Droplet(config: config) 57 | 58 | XCTAssert(drop.cache is RedisCache) 59 | } 60 | 61 | func testDatabaseSelect() throws { 62 | var config = Config([:]) 63 | try config.set("droplet.cache", "redis") 64 | try config.set("redis.hostname", RedisCache.testAddress) 65 | try config.set("redis.port", RedisCache.testPort) 66 | try config.set("redis.database", 2) 67 | try config.addProvider(RedisProvider.Provider.self) 68 | 69 | let drop = try Droplet(config: config) 70 | 71 | let key = UUID().uuidString 72 | try drop.cache.set(key, "bar") 73 | 74 | XCTAssertEqual("bar", try drop.cache.get(key)?.string) 75 | 76 | // Verify that the value is in fact stored in database 2 77 | let cache = try RedisCache( 78 | hostname: RedisCache.testAddress, 79 | port: RedisCache.testPort 80 | ) 81 | 82 | let client = try cache.makeClient() 83 | XCTAssertNil(try client.command(Command.get, [key])?.string) 84 | try client.command(try Command("select"), ["2"]) 85 | XCTAssertEqual("bar", try client.command(Command.get, [key])?.string) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/RedisProviderTests/RedisCacheTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import RedisProvider 3 | @testable import Node 4 | 5 | class RedisCacheTests: XCTestCase { 6 | static let allTests = [ 7 | ("testBasic", testBasic), 8 | ("testMissing", testMissing), 9 | ("testArray", testArray), 10 | ("testBytes", testBytes), 11 | ("testObject", testObject), 12 | ("testDelete", testDelete), 13 | ("testExpire", testExpire) 14 | ] 15 | 16 | var cache: RedisCache! 17 | 18 | override func setUp() { 19 | cache = RedisCache.makeForTesting() 20 | } 21 | 22 | func testBasic() throws { 23 | try cache.set("hello", "world") 24 | XCTAssertEqual(try cache.get("hello")?.string, "world") 25 | } 26 | 27 | func testMissing() throws { 28 | XCTAssertEqual(try cache.get("not-here"), nil) 29 | } 30 | 31 | func testArray() throws { 32 | try cache.set("array", [1]) 33 | 34 | let array = try cache.get("array")?.array 35 | let value: Int = (array?[0].int) ?? 0 36 | 37 | XCTAssertEqual(value, 1) 38 | } 39 | 40 | func testBytes() throws { 41 | try cache.set("bytes", Node(bytes: [1, 2, 3])) 42 | 43 | let bytes: Bytes = try cache.get("bytes")?.bytes ?? [] 44 | 45 | XCTAssertEqual(bytes, [1, 2, 3]) 46 | } 47 | 48 | func testObject() throws { 49 | try cache.set("object", Node([ 50 | "key0": "value0", 51 | "key1": "value1" 52 | ])) 53 | 54 | let node = try cache.get("object") 55 | let value0: String = (node?["key0"]?.string) ?? "wrong" 56 | let value1: String = (node?["key1"]?.string) ?? "wrong" 57 | 58 | XCTAssertEqual(value0, "value0") 59 | XCTAssertEqual(value1, "value1") 60 | } 61 | 62 | func testDelete() throws { 63 | try cache.set("hello", 42) 64 | XCTAssertEqual(try cache.get("hello")?.int, 42) 65 | try cache.delete("hello") 66 | XCTAssertEqual(try cache.get("hello"), nil) 67 | } 68 | 69 | func testExpire() throws { 70 | try cache.set("ephemeral", 42, expiration: Date(timeIntervalSinceNow: 2)) 71 | XCTAssertEqual(try cache.get("ephemeral")?.int, 42) 72 | sleep(3) 73 | XCTAssertEqual(try cache.get("ephemeral"), nil) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/RedisProviderTests/Utilities.swift: -------------------------------------------------------------------------------- 1 | import RedisProvider 2 | import XCTest 3 | import Transport 4 | 5 | 6 | extension RedisCache { 7 | static var testPort: Transport.Port { 8 | return 6379 9 | } 10 | 11 | static var testAddress: String { 12 | return "127.0.0.1" 13 | } 14 | 15 | static func makeForTesting() -> RedisCache { 16 | do { 17 | return try RedisCache(hostname: testAddress, port: testPort) 18 | } catch { 19 | print() 20 | print() 21 | print("⚠️ Redis Not Configured ⚠️") 22 | print() 23 | print("Error: \(error)") 24 | print() 25 | print("You must configure Redis to run with the following configuration: ") 26 | print(" host: '\(testAddress)'") 27 | print(" port: '\(testPort)'") 28 | print(" password: none") 29 | print() 30 | 31 | print() 32 | 33 | XCTFail("Configure Redis") 34 | fatalError("Configure Redis") 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------