Config and config loading interface

This commit is contained in:
Alexandr Goncharov 2020-06-09 21:15:06 +03:00
parent fb71a901db
commit d49b82d055
6 changed files with 120 additions and 12 deletions

45
Sources/Conf/Config.swift Normal file
View File

@ -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 })
}
}

View File

@ -0,0 +1,5 @@
enum ConfigurationError: Error {
case fetch(Error)
case parse(Error)
case `import`(path: Key, value: Any)
}

View File

@ -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)
}
}
}

View File

@ -35,7 +35,7 @@ public struct Environment {
return raw.flatMap(Value.init)
}
nonmutating set (value) {
self[key] = value?.description ?? nil
self[key] = value?.description
}
}

View File

@ -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)>"
}
}

View File

@ -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"])
}
}