Config and config loading interface
This commit is contained in:
parent
fb71a901db
commit
d49b82d055
|
@ -0,0 +1,45 @@
|
|||
/// Key-value storage that can be filled from multiple sources
|
||||
public final class Config {
|
||||
public init(useEnvironment: Bool = false) {
|
||||
isBackedByEnvironment = useEnvironment
|
||||
}
|
||||
|
||||
let isBackedByEnvironment: Bool
|
||||
private let environment = Environment()
|
||||
private var data = [Key: String]()
|
||||
|
||||
func value(for key: Key) -> String? {
|
||||
return data[key] ?? envValue(for: key)
|
||||
}
|
||||
|
||||
func envValue(for key: Key) -> String? {
|
||||
guard isBackedByEnvironment,
|
||||
key.path.count == 1,
|
||||
let variable = key.path.first
|
||||
else { return nil }
|
||||
return environment[variable]
|
||||
}
|
||||
|
||||
func set(value: String?, for key: Key) {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
public subscript(_ key: Key) -> String? {
|
||||
get { value(for: key) }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
public subscript<Value>(_ key: Key) -> Value? where Value: LosslessStringConvertible {
|
||||
get { value(for: key).flatMap(Value.init) }
|
||||
set { set(value: newValue?.description, for: key) }
|
||||
}
|
||||
|
||||
public func dump() -> [Key: String] {
|
||||
data
|
||||
}
|
||||
|
||||
func loadConfiguration(from provider: ConfigurationProvider) throws {
|
||||
try data.merge(provider.configuration(),
|
||||
uniquingKeysWith: { _, new in new })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
enum ConfigurationError: Error {
|
||||
case fetch(Error)
|
||||
case parse(Error)
|
||||
case `import`(path: Key, value: Any)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
protocol ConfigurationProvider {
|
||||
func configuration() throws -> [Key: String]
|
||||
}
|
||||
|
||||
final class CommonConfigurationProvider: ConfigurationProvider {
|
||||
typealias Fetcher = () throws -> String
|
||||
typealias Parser = (String) throws -> [String: Any]
|
||||
|
||||
init(loader: @escaping Fetcher, parser: @escaping Parser) {
|
||||
self.load = loader
|
||||
self.parse = parser
|
||||
}
|
||||
|
||||
let load: Fetcher
|
||||
let parse: Parser
|
||||
|
||||
func configuration() throws -> [Key: String] {
|
||||
let rawData: String
|
||||
let parsedData: [String: Any]
|
||||
do {
|
||||
try rawData = load()
|
||||
} catch { throw ConfigurationError.fetch(error) }
|
||||
do {
|
||||
parsedData = try parse(rawData)
|
||||
} catch { throw ConfigurationError.parse(error) }
|
||||
return try decode(currentKey: Key(), object: parsedData)
|
||||
}
|
||||
|
||||
func decode(currentKey: Key, object: Any) throws -> [Key: String] {
|
||||
switch object {
|
||||
case let value as LosslessStringConvertible:
|
||||
return [currentKey: value.description]
|
||||
case let value as [String: LosslessStringConvertible]:
|
||||
return .init(uniqueKeysWithValues:
|
||||
value.map { key, value in
|
||||
(currentKey.child(key: key), value.description) })
|
||||
case let dictionary as [String: Any]:
|
||||
return try dictionary.map { key, value in
|
||||
try decode(currentKey: currentKey.child(key: key), object: value)
|
||||
}.reduce(into: [Key: String]()) { result, value in
|
||||
result.merge(value) { current, _ in current }
|
||||
}
|
||||
case let array as [LosslessStringConvertible]:
|
||||
return .init(uniqueKeysWithValues:
|
||||
array.enumerated()
|
||||
.map { ( currentKey.child(key: String($0)), $1.description) })
|
||||
default:
|
||||
throw ConfigurationError.import(path: currentKey, value: object)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ public struct Environment {
|
|||
return raw.flatMap(Value.init)
|
||||
}
|
||||
nonmutating set (value) {
|
||||
self[key] = value?.description ?? nil
|
||||
self[key] = value?.description
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
/// Configuration key that points to the value
|
||||
public struct Key {
|
||||
|
||||
let fragmets: [String]
|
||||
let path: [String]
|
||||
|
||||
public init(_ value: LosslessStringConvertible) {
|
||||
fragmets = [value.description]
|
||||
path = [value.description]
|
||||
}
|
||||
|
||||
public init(_ elements: [LosslessStringConvertible]) {
|
||||
fragmets = elements.map(\.description)
|
||||
path = elements.map(\.description)
|
||||
}
|
||||
|
||||
public init(_ elements: LosslessStringConvertible...) {
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
func child(key: String) -> Key {
|
||||
Key(path + [key])
|
||||
}
|
||||
}
|
||||
|
||||
extension Key: Equatable {}
|
||||
|
@ -40,6 +43,6 @@ extension Key: ExpressibleByIntegerLiteral {
|
|||
|
||||
extension Key: CustomStringConvertible {
|
||||
public var description: String {
|
||||
"Key<\(fragmets.description)>"
|
||||
"Key<\(path.description)>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,22 @@ import XCTest
|
|||
final class KeyTests: XCTestCase {
|
||||
func testCreation() {
|
||||
let key1: Key = Key()
|
||||
XCTAssertEqual(key1.fragmets, [])
|
||||
XCTAssertEqual(key1.path, [])
|
||||
let key2: Key = "24"
|
||||
XCTAssertEqual(key2.fragmets, ["24"])
|
||||
XCTAssertEqual(key2.path, ["24"])
|
||||
let key3: Key = 99
|
||||
XCTAssertEqual(key3.fragmets, ["99"])
|
||||
XCTAssertEqual(key3.path, ["99"])
|
||||
let key4: Key = Key(23.4)
|
||||
XCTAssertEqual(key4.fragmets, ["23.4"])
|
||||
XCTAssertEqual(key4.path, ["23.4"])
|
||||
let key5: Key = Key("some")
|
||||
XCTAssertEqual(key5.fragmets, ["some"])
|
||||
XCTAssertEqual(key5.path, ["some"])
|
||||
let key6: Key = ["24", 72, 23.4, true]
|
||||
XCTAssertEqual(key6.fragmets, ["24", "72", "23.4", "true"])
|
||||
XCTAssertEqual(key6.path, ["24", "72", "23.4", "true"])
|
||||
let key7: Key = Key([1, 2, 3])
|
||||
XCTAssertEqual(key7.fragmets, ["1", "2", "3"])
|
||||
XCTAssertEqual(key7.path, ["1", "2", "3"])
|
||||
}
|
||||
|
||||
func testChild() {
|
||||
XCTAssertEqual(Key("hello").child(key: "world").path, ["hello", "world"])
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue