Merge pull request 'Basic implementation' (#3) from develop into master
Reviewed-on: http://code.merlin.local:3000/adan/Conf/pulls/3
This commit is contained in:
commit
a7b49b69e6
|
@ -6,20 +6,11 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "Conf",
|
||||
products: [
|
||||
.library(name: "Conf", targets: ["Conf"]),
|
||||
// .executable(name: "Demo", targets: ["DemoApp"])
|
||||
.library(name: "Conf", targets: ["Conf"])
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "DemoApp",
|
||||
dependencies: [
|
||||
"Conf",
|
||||
]),
|
||||
.testTarget(
|
||||
name: "DemoAppTests",
|
||||
dependencies: ["DemoApp"]),
|
||||
.target(
|
||||
name: "Conf",
|
||||
dependencies: [
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import Foundation
|
||||
|
||||
public extension Config {
|
||||
func load(env filename: String) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.donEnv))
|
||||
}
|
||||
|
||||
func load(env data: Data) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.donEnv))
|
||||
}
|
||||
|
||||
func load(json filename: String) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.json))
|
||||
}
|
||||
|
||||
func load(json data: Data) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.json))
|
||||
}
|
||||
|
||||
func load(plist filename: String) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.file(filename), parser: Parser.plist))
|
||||
}
|
||||
|
||||
func load(plist data: Data) throws {
|
||||
try load(from: DefaultConfigurationProvider(loader: Fetcher.direct(data), parser: Parser.plist))
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
public func load(from provider: ConfigurationProvider) throws {
|
||||
try data.merge(provider.configuration(),
|
||||
uniquingKeysWith: { _, new in new })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
public enum ConfigurationError: Error {
|
||||
case fetch(Error)
|
||||
case parse(Error)
|
||||
case decode(path: Key, value: Any)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import Foundation
|
||||
|
||||
public protocol ConfigurationProvider {
|
||||
func configuration() throws -> [Key: String]
|
||||
}
|
||||
|
||||
final class DefaultConfigurationProvider: ConfigurationProvider {
|
||||
typealias Fetcher = () throws -> Data
|
||||
typealias Parser = (Data) 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: Data
|
||||
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.decode(path: currentKey, value: object)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import Foundation
|
||||
|
||||
/// Manages environment variables
|
||||
@dynamicMemberLookup
|
||||
public struct Environment {
|
||||
public init(info: ProcessInfo = .processInfo) {
|
||||
self.info = info
|
||||
}
|
||||
|
||||
private let info: ProcessInfo
|
||||
|
||||
public func dump() -> [String: String] {
|
||||
info.environment
|
||||
}
|
||||
|
||||
public subscript(_ key: String) -> String? {
|
||||
get { info.environment[key] }
|
||||
nonmutating set (value) {
|
||||
if let raw = value {
|
||||
setenv(key, raw, 1)
|
||||
} else {
|
||||
unsetenv(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public subscript(dynamicMember member: String) -> String? {
|
||||
get { self[member] }
|
||||
nonmutating set { self[member] = newValue }
|
||||
}
|
||||
|
||||
public subscript<Value>(_ key: String) -> Value? where Value: LosslessStringConvertible {
|
||||
get {
|
||||
let raw: String? = self[key]
|
||||
return raw.flatMap(Value.init)
|
||||
}
|
||||
nonmutating set (value) {
|
||||
self[key] = value?.description
|
||||
}
|
||||
}
|
||||
|
||||
public subscript<Value>(dynamicMember member: String) -> Value? where Value: LosslessStringConvertible {
|
||||
get { self[member] }
|
||||
nonmutating set (value) { self[member] = value }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import Foundation
|
||||
|
||||
/// Namespace for the predefined fetchers
|
||||
enum Fetcher { }
|
||||
|
||||
extension Fetcher {
|
||||
|
||||
static let direct: (Data) -> DefaultConfigurationProvider.Fetcher = { data in
|
||||
return { data }
|
||||
}
|
||||
|
||||
static let file: (String) -> DefaultConfigurationProvider.Fetcher = { configName in
|
||||
return {
|
||||
let url = URL(fileURLWithPath: configName, isDirectory: false)
|
||||
print(url)
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
public func foo() -> Int {
|
||||
return 42
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/// Configuration key that points to the value
|
||||
public struct Key {
|
||||
|
||||
let path: [String]
|
||||
|
||||
public init(_ value: LosslessStringConvertible) {
|
||||
path = [value.description]
|
||||
}
|
||||
|
||||
public init(_ elements: [LosslessStringConvertible]) {
|
||||
path = elements.map(\.description)
|
||||
}
|
||||
|
||||
public init(_ elements: LosslessStringConvertible...) {
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
func child(key: String) -> Key {
|
||||
Key(path + [key])
|
||||
}
|
||||
}
|
||||
|
||||
extension Key: Equatable {}
|
||||
extension Key: Hashable {}
|
||||
|
||||
extension Key: ExpressibleByArrayLiteral {
|
||||
public init(arrayLiteral elements: LosslessStringConvertible...) {
|
||||
self.init(elements)
|
||||
}
|
||||
}
|
||||
|
||||
extension Key: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Key: ExpressibleByIntegerLiteral {
|
||||
public init(integerLiteral value: Int) {
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Key: CustomStringConvertible {
|
||||
public var description: String {
|
||||
"Key<\(path.description)>"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import Foundation
|
||||
|
||||
/// Namespace for the predefined parsers
|
||||
enum Parser {
|
||||
struct InvalidFormat: Error { let data: Data }
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
static let json: DefaultConfigurationProvider.Parser = { data in
|
||||
let object = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
guard let values = object as? [String: Any] else {
|
||||
throw InvalidFormat(data: data)
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
static let donEnv: DefaultConfigurationProvider.Parser = { data in
|
||||
func fail() throws -> Never {
|
||||
throw InvalidFormat(data: data)
|
||||
}
|
||||
guard let contents = String(data: data, encoding: .utf8) else { try fail() }
|
||||
var result = [String: String]()
|
||||
let lines = contents.split(whereSeparator: { $0 == "\n" || $0 == "\r\n" }).lazy
|
||||
for line in lines {
|
||||
// ignore comments
|
||||
if line.starts(with: "#") { continue }
|
||||
// ignore lines that appear empty
|
||||
if line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
continue
|
||||
}
|
||||
// extract key and value which are separated by an equals sign
|
||||
let parts = line.split(separator: "=", maxSplits: 1)
|
||||
guard parts.count == 2 else { try fail() }
|
||||
let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
// remove surrounding quotes from value & convert remove escape character before any embedded quotes
|
||||
if value[value.startIndex] == "\"" && value[value.index(before: value.endIndex)] == "\"" {
|
||||
value.remove(at: value.startIndex)
|
||||
value.remove(at: value.index(before: value.endIndex))
|
||||
value = value.replacingOccurrences(of: "\\\"", with: "\"")
|
||||
}
|
||||
// remove surrounding single quotes from value & convert remove escape character before any embedded quotes
|
||||
if value[value.startIndex] == "'" && value[value.index(before: value.endIndex)] == "'" {
|
||||
value.remove(at: value.startIndex)
|
||||
value.remove(at: value.index(before: value.endIndex))
|
||||
value = value.replacingOccurrences(of: "'", with: "'")
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
static let plist: DefaultConfigurationProvider.Parser = { data in
|
||||
let object = try PropertyListSerialization.propertyList(from: data, format: nil)
|
||||
guard let values = object as? [String: Any] else {
|
||||
throw InvalidFormat(data: data)
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import Conf
|
||||
|
||||
print("Hello, world!")
|
|
@ -0,0 +1,72 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class ConfigTests: XCTestCase {
|
||||
|
||||
var config: Config!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
config = Config()
|
||||
}
|
||||
|
||||
func testEnvironmentuse() {
|
||||
XCTAssertNil(Config(useEnvironment: false)["PATH"])
|
||||
XCTAssertNotNil(Config(useEnvironment: true)["PATH"])
|
||||
}
|
||||
|
||||
func testLoadConfigThrow() {
|
||||
let errorConfigProvider = MockConfigurationProvider(config: [:], error: TestError())
|
||||
|
||||
XCTAssertThrowsError(try config.load(from: errorConfigProvider)) { error in
|
||||
XCTAssertTrue(error is TestError)
|
||||
}
|
||||
}
|
||||
|
||||
func testLoadConfigSuccess() throws {
|
||||
let loader = MockConfigurationProvider(config: ["key": "value"])
|
||||
try config.load(from: loader)
|
||||
XCTAssertEqual(config.dump(), [Key("key"): "value"])
|
||||
}
|
||||
|
||||
func testWrite() {
|
||||
let key: Key = "key"
|
||||
XCTAssertEqual(config.dump(), [:])
|
||||
config.set(value: "value", for: key)
|
||||
XCTAssertEqual(config.dump(), [Key("key"): "value"])
|
||||
config.set(value: nil, for: key)
|
||||
XCTAssertEqual(config.dump(), [:])
|
||||
config.set(value: "another value", for: ["long", "path"])
|
||||
XCTAssertEqual(config.dump(), [Key(["long", "path"]): "another value"])
|
||||
|
||||
config["subscript"] = "subscriptValue"
|
||||
XCTAssertEqual(config.dump()["subscript"], "subscriptValue")
|
||||
|
||||
config["lossless"] = 42
|
||||
XCTAssertEqual(config.dump()["lossless"], "42")
|
||||
}
|
||||
|
||||
func testRead() throws {
|
||||
let loader = MockConfigurationProvider(config: [
|
||||
"string": "value",
|
||||
"int": "42",
|
||||
"double": "23.5"
|
||||
])
|
||||
try config.load(from: loader)
|
||||
XCTAssertEqual(config["blabla"], nil)
|
||||
XCTAssertEqual(config["string"], "value")
|
||||
XCTAssertEqual(config["int"], "42")
|
||||
XCTAssertEqual(config["int"], 42)
|
||||
XCTAssertEqual(config["double"], "23.5")
|
||||
XCTAssertEqual(config["double"], 23.5)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct MockConfigurationProvider: ConfigurationProvider {
|
||||
var config: [Key: String]
|
||||
var error: Error?
|
||||
func configuration() throws -> [Key: String] {
|
||||
if let error = self.error { throw error }
|
||||
return config
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class CommonConfigurationProviderTests: XCTestCase {
|
||||
func testFetchError() {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { throw TestError() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in [:] }
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
|
||||
XCTAssertThrowsError(try provider.configuration()) { error in
|
||||
if case let ConfigurationError.fetch(detailError) = error {
|
||||
XCTAssertTrue(detailError is TestError)
|
||||
} else {
|
||||
XCTFail("Invalid error returned \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testParserError() {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in throw TestError() }
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
|
||||
XCTAssertThrowsError(try provider.configuration()) { error in
|
||||
if case let ConfigurationError.parse(detailError) = error {
|
||||
XCTAssertTrue(detailError is TestError)
|
||||
} else {
|
||||
XCTFail("Invalid error returned \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDecodeError() {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let uuid = UUID()
|
||||
let parser: DefaultConfigurationProvider.Parser = { data in
|
||||
return [
|
||||
"first": "value",
|
||||
"second": uuid
|
||||
]
|
||||
}
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
|
||||
XCTAssertThrowsError(try provider.configuration()) { error in
|
||||
if case let ConfigurationError.decode(path: key, value: value) = error {
|
||||
XCTAssertEqual(key, "second")
|
||||
XCTAssertEqual(value as? UUID, uuid)
|
||||
} else {
|
||||
XCTFail("Invalid error returned \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDataFlow() throws {
|
||||
let data = "string".data(using: .utf8)!
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { data }
|
||||
let parser: DefaultConfigurationProvider.Parser = { parserInput in
|
||||
XCTAssertEqual(parserInput, data)
|
||||
return [:]
|
||||
}
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
XCTAssertTrue(configuration.isEmpty)
|
||||
}
|
||||
|
||||
func testDecodeValues() throws {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in
|
||||
return [ "key": 22]
|
||||
}
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
XCTAssertEqual(configuration, ["key": "22"])
|
||||
}
|
||||
|
||||
func testDecodeArray() throws {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in
|
||||
return [ "key": ["one", "two"]]
|
||||
}
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
let expect: [Key: String] = [
|
||||
Key(["key", "0"]): "one",
|
||||
Key(["key", "1"]): "two"
|
||||
]
|
||||
XCTAssertEqual(configuration, expect)
|
||||
}
|
||||
|
||||
func testDecodeNestedValues() throws {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in
|
||||
return [
|
||||
"key": [ "nested": "value" ],
|
||||
"one": [ "more": [
|
||||
"deep": "value"
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
let expect: [Key: String] = [
|
||||
Key(["key", "nested"]): "value",
|
||||
Key(["one", "more", "deep"]): "value"
|
||||
]
|
||||
XCTAssertEqual(configuration, expect)
|
||||
}
|
||||
|
||||
func testDecodeEmptyValues() throws {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in [:] }
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
XCTAssertEqual(configuration, [:])
|
||||
}
|
||||
|
||||
func testDecodeEmptyArray() throws {
|
||||
let fetcher: DefaultConfigurationProvider.Fetcher = { Data() }
|
||||
let parser: DefaultConfigurationProvider.Parser = { _ in [ "key": []] }
|
||||
let provider = DefaultConfigurationProvider(loader: fetcher, parser: parser)
|
||||
let configuration = try provider.configuration()
|
||||
XCTAssertEqual(configuration, [:])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class DotEnvParserTests: XCTestCase {
|
||||
func testSuccess() throws {
|
||||
let content = try Resource(name: "valid", type: "env").data()
|
||||
let result = try Parser.donEnv(content)
|
||||
let expect = [
|
||||
"TZ": "Europe/Moscow",
|
||||
"PUID": "1026",
|
||||
"PGID": "100",
|
||||
"IP": "127.0.0.1"
|
||||
]
|
||||
XCTAssertEqual(result as? [String: String], expect)
|
||||
}
|
||||
|
||||
func testError() throws {
|
||||
let content = try Resource(name: "invalid", type: "env").data()
|
||||
XCTAssertThrowsError(try Parser.donEnv(content)) { error in
|
||||
XCTAssertTrue(error is Parser.InvalidFormat)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class EnvironmentTests: XCTestCase {
|
||||
|
||||
static let sampleValues: [String: String] = [
|
||||
"first": "string",
|
||||
"integer": "1",
|
||||
"float": "2.33",
|
||||
"booleanTrue": "true",
|
||||
"booleanFalse": "false",
|
||||
"YES": "YES"
|
||||
]
|
||||
|
||||
func testDump() throws {
|
||||
let values = Self.sampleValues
|
||||
let info = ProcessInfo.make(with: values)
|
||||
let env = Environment(info: info)
|
||||
|
||||
let dumped = env.dump()
|
||||
|
||||
XCTAssertEqual(values, dumped)
|
||||
}
|
||||
|
||||
func testRead() throws {
|
||||
let info = ProcessInfo.make(with: Self.sampleValues)
|
||||
let env = Environment(info: info)
|
||||
|
||||
XCTAssertNil(env.blabla)
|
||||
XCTAssertNil(env["blabla"])
|
||||
|
||||
XCTAssertEqual(env.first, "string")
|
||||
XCTAssertNil(env.first as Int?)
|
||||
XCTAssertEqual(env["first"], "string")
|
||||
XCTAssertNil(env["first"] as Int?)
|
||||
|
||||
XCTAssertEqual(env.integer as Int?, 1)
|
||||
XCTAssertEqual(env.integer, "1")
|
||||
XCTAssertEqual(env["integer"] as Int?, 1)
|
||||
XCTAssertEqual(env["integer"], "1")
|
||||
|
||||
XCTAssertEqual(env.float, "2.33")
|
||||
XCTAssertEqual(env.float as Double?, 2.33)
|
||||
XCTAssertEqual(env.float as Float?, 2.33)
|
||||
XCTAssertNil(env.float as Int?)
|
||||
XCTAssertEqual(env["float"], "2.33")
|
||||
XCTAssertEqual(env["float"] as Double?, 2.33)
|
||||
XCTAssertEqual(env["float"] as Float?, 2.33)
|
||||
XCTAssertNil(env["float"] as Int?)
|
||||
|
||||
XCTAssertEqual(env.booleanTrue, "true")
|
||||
XCTAssertEqual(env.booleanTrue as Bool?, true)
|
||||
XCTAssertNil(env.booleanTrue as Int?)
|
||||
XCTAssertEqual(env["booleanTrue"], "true")
|
||||
XCTAssertEqual(env["booleanTrue"] as Bool?, true)
|
||||
XCTAssertNil(env["booleanTrue"] as Int?)
|
||||
|
||||
XCTAssertEqual(env.booleanFalse, "false")
|
||||
XCTAssertEqual(env.booleanFalse as Bool?, false)
|
||||
XCTAssertNil(env.booleanFalse as Int?)
|
||||
XCTAssertEqual(env["booleanFalse"], "false")
|
||||
XCTAssertEqual(env["booleanFalse"] as Bool?, false)
|
||||
XCTAssertNil(env["booleanFalse"] as Int?)
|
||||
|
||||
XCTAssertEqual(env.YES, "YES")
|
||||
XCTAssertNil(env.YES as Bool?)
|
||||
}
|
||||
|
||||
func testAdd() {
|
||||
let env = Environment(info: ProcessInfo.instance)
|
||||
|
||||
XCTAssertNil(env.key1)
|
||||
env.key1 = "value1"
|
||||
XCTAssertEqual(env.key1, "value1")
|
||||
|
||||
XCTAssertNil(env.key2)
|
||||
env["key2"] = "value2"
|
||||
XCTAssertEqual(env.key2, "value2")
|
||||
}
|
||||
|
||||
func testRemove() {
|
||||
let env = Environment(info: ProcessInfo.instance)
|
||||
env.key1 = "value1"
|
||||
|
||||
XCTAssertEqual(env.key1, "value1")
|
||||
env.key1 = nil
|
||||
XCTAssertNil(env.key1)
|
||||
|
||||
env["key2"] = "value2"
|
||||
XCTAssertEqual(env["key2"], "value2")
|
||||
env["key2"] = nil
|
||||
XCTAssertNil(env["key2"])
|
||||
}
|
||||
|
||||
func testUpdate() {
|
||||
let env = Environment(info: ProcessInfo.instance)
|
||||
env.key1 = "value"
|
||||
|
||||
XCTAssertEqual(env.key1, "value")
|
||||
env.key1 = "new value"
|
||||
XCTAssertEqual(env.key1, "new value")
|
||||
}
|
||||
|
||||
func testWrite() {
|
||||
let env = Environment(info: ProcessInfo.instance)
|
||||
|
||||
let int = Int.random(in: Int.min...Int.max)
|
||||
env.int = int
|
||||
XCTAssertEqual(env.int, int)
|
||||
|
||||
let float = Float.random(in: 0...100)
|
||||
env.float = float
|
||||
XCTAssertEqual(env.float, float)
|
||||
|
||||
env.success = true
|
||||
XCTAssertEqual(env.success, true)
|
||||
|
||||
env.failure = false
|
||||
XCTAssertEqual(env.failure, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class FileFetcherTests: XCTestCase {
|
||||
func testSuccess() throws {
|
||||
let load = Fetcher.file("Tests/Resources/valid.env")
|
||||
_ = try load()
|
||||
}
|
||||
|
||||
func testError() throws {
|
||||
let load = Fetcher.file("file that not exist")
|
||||
XCTAssertThrowsError(try load())
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class FooTests: XCTestCase {
|
||||
|
||||
func testFoo() throws {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class JSONParserTests: XCTestCase {
|
||||
func testSuccess() throws {
|
||||
let content = try Resource(name: "valid", type: "json").data()
|
||||
_ = try Parser.json(content)
|
||||
}
|
||||
|
||||
func testError() throws {
|
||||
let content = try Resource(name: "invalid", type: "json").data()
|
||||
XCTAssertThrowsError(try Parser.json(content)) { error in
|
||||
XCTAssertTrue(error is Parser.InvalidFormat)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class KeyTests: XCTestCase {
|
||||
func testCreation() {
|
||||
let key1: Key = Key()
|
||||
XCTAssertEqual(key1.path, [])
|
||||
let key2: Key = "24"
|
||||
XCTAssertEqual(key2.path, ["24"])
|
||||
let key3: Key = 99
|
||||
XCTAssertEqual(key3.path, ["99"])
|
||||
let key4: Key = Key(23.4)
|
||||
XCTAssertEqual(key4.path, ["23.4"])
|
||||
let key5: Key = Key("some")
|
||||
XCTAssertEqual(key5.path, ["some"])
|
||||
let key6: Key = ["24", 72, 23.4, true]
|
||||
XCTAssertEqual(key6.path, ["24", "72", "23.4", "true"])
|
||||
let key7: Key = Key([1, 2, 3])
|
||||
XCTAssertEqual(key7.path, ["1", "2", "3"])
|
||||
}
|
||||
|
||||
func testChild() {
|
||||
XCTAssertEqual(Key("hello").child(key: "world").path, ["hello", "world"])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
class MockProcessInfo: ProcessInfo {
|
||||
init(_ env: [String: String]) { self.env = env }
|
||||
var env: [String: String]
|
||||
override var environment: [String: String] { env }
|
||||
}
|
||||
#endif
|
||||
|
||||
extension ProcessInfo {
|
||||
static var instance: ProcessInfo {
|
||||
#if os(macOS)
|
||||
return ProcessInfo()
|
||||
#else
|
||||
return ProcessInfo.processInfo
|
||||
#endif
|
||||
}
|
||||
|
||||
static func make(with env: [String: String]) -> ProcessInfo {
|
||||
#if os(macOS)
|
||||
return MockProcessInfo(env)
|
||||
#else
|
||||
let info = ProcessInfo.instance
|
||||
info.clearEnv()
|
||||
info.set(env: env)
|
||||
return info
|
||||
#endif
|
||||
}
|
||||
|
||||
func clearEnv() {
|
||||
environment.forEach {key, _ in
|
||||
unsetenv(key)
|
||||
}
|
||||
}
|
||||
|
||||
func set(env: [String: String]) {
|
||||
env.forEach { key, value in
|
||||
setenv(key, value, 1)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import XCTest
|
||||
@testable import Conf
|
||||
|
||||
final class PlistParserTests: XCTestCase {
|
||||
func testSuccess() throws {
|
||||
let content = try Resource(name: "valid", type: "plist").data()
|
||||
_ = try Parser.plist(content)
|
||||
}
|
||||
|
||||
func testError() throws {
|
||||
let content = try Resource(name: "invalid", type: "plist").data()
|
||||
XCTAssertThrowsError(try Parser.plist(content)) { error in
|
||||
XCTAssertTrue(error is Parser.InvalidFormat)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
|
||||
struct Resource {
|
||||
let name: String
|
||||
let type: String
|
||||
let url: URL
|
||||
|
||||
init(name: String, type: String) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
url = Resource.resourceFolderURL.appendingPathComponent(name).appendingPathExtension(type)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Content -
|
||||
extension Resource {
|
||||
func data() throws -> Data { try Data(contentsOf: url) }
|
||||
func string() throws -> String { try String(contentsOf: url, encoding: .utf8) }
|
||||
}
|
||||
|
||||
// MARK: - Path helpers -
|
||||
extension Resource {
|
||||
// expected folder structure
|
||||
// <Some folder>
|
||||
// - <Resources>
|
||||
// - <resource files>
|
||||
// - <Some test source folder>
|
||||
// - <test case files>
|
||||
// - <this file>
|
||||
static let resourceFolderURL = testsFolderURL
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent(resourceFolder, isDirectory: true)
|
||||
.standardized
|
||||
static let testsFolderURL = sourceFileURL.deletingLastPathComponent()
|
||||
private static let resourceFolder = "Resources"
|
||||
private static let sourceFileURL = URL(fileURLWithPath: #file, isDirectory: false)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
struct TestError: Error {}
|
|
@ -1,43 +0,0 @@
|
|||
import XCTest
|
||||
import class Foundation.Bundle
|
||||
|
||||
final class OutputTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
|
||||
// Some of the APIs that we use below are available in macOS 10.13 and above.
|
||||
guard #available(macOS 10.13, *) else {
|
||||
return
|
||||
}
|
||||
|
||||
let fooBinary = productsDirectory.appendingPathComponent("DemoApp")
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = fooBinary
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8)
|
||||
|
||||
XCTAssertEqual(output, "Hello, world!\n")
|
||||
}
|
||||
|
||||
/// Returns path to the built products directory.
|
||||
var productsDirectory: URL {
|
||||
#if os(macOS)
|
||||
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
|
||||
return bundle.bundleURL.deletingLastPathComponent()
|
||||
}
|
||||
fatalError("couldn't find the products directory")
|
||||
#else
|
||||
return Bundle.main.bundleURL
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
## Sample invalid env file
|
||||
Hello
|
|
@ -0,0 +1 @@
|
|||
[1, 2, 3]
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<array>
|
||||
<string>one</string>
|
||||
<string>two</string>
|
||||
</array>
|
||||
</plist>
|
|
@ -0,0 +1,8 @@
|
|||
# Sample env file
|
||||
TZ="Europe/Moscow"
|
||||
|
||||
IP='127.0.0.1'
|
||||
|
||||
# UNIX PUID and PGID, find with: id $USER
|
||||
PUID=1026
|
||||
PGID=100
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"param": "value",
|
||||
"nested": {
|
||||
"key": "values"
|
||||
},
|
||||
"array": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
Loading…
Reference in New Issue