├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
--------------------------------------------------------------------------------