Skip to content

Commit

Permalink
Optional strings (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebskuse authored and dhardiman committed Apr 30, 2019
1 parent 4e10f39 commit 301123d
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 20 deletions.
2 changes: 2 additions & 0 deletions Sources/Config/ConfigurationFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ func parseNextProperty(properties: [String: Property], pair: (key: String, value
switch typeHint {
case .string, .url, .encrypted, .encryptionKey, .colour, .image, .regex:
copy[pair.key] = ConfigurationProperty<String>(key: pair.key, typeHint: typeHintValue, dict: dict)
case .optionalString:
copy[pair.key] = ConfigurationProperty<String?>(key: pair.key, typeHint: typeHintValue, dict: dict)
case .double, .float:
copy[pair.key] = ConfigurationProperty<Double>(key: pair.key, typeHint: typeHintValue, dict: dict)
case .int:
Expand Down
52 changes: 40 additions & 12 deletions Sources/Config/ConfigurationProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import Foundation

struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
private enum Failure: Error {
case notConvertible
}

let key: String
let description: String?
Expand All @@ -26,21 +29,46 @@ struct ConfigurationProperty<T>: Property, AssociatedPropertyKeyProviding {
}
}

/// Returns `value` as the `ConfigurationProperty`'s value type (T).
/// If T is Optional<Something> and conversion of `value` fails,
/// rather than throwing an exception and bailing out this method
/// will return `Optional.none` using `ExpressibleByNilLiteral`'s
/// init(nilLiteral:), allowing a `ConfigurationProperty` with a nil
/// value.
///
/// - Parameter value: The value to transform.
/// - Returns: The value as T, if possible.
/// - Throws: If the value is not convertible to T, Failure.notConvertible
/// will be thrown.
private static func transformValueToType(value: Any?) throws -> T {
if let val = value as? T {
return val
}
if let nilLiteralType = T.self as? ExpressibleByNilLiteral.Type {
return nilLiteralType.init(nilLiteral: ()) as! T
}

throw Failure.notConvertible
}

init?(key: String, typeHint: String, dict: [String: Any]) {
guard let defaultValue = dict["defaultValue"] as? T else {
do {
self.defaultValue = try ConfigurationProperty.transformValueToType(value: dict["defaultValue"])

self.key = key
self.typeHint = typeHint
self.associatedProperty = dict["associatedProperty"] as? String
self.type = PropertyType(rawValue: typeHint)
self.description = dict["description"] as? String

let overrides = try? dict["overrides"]
.flatMap { $0 as? [String: Any] }?
.mapValues { try ConfigurationProperty.transformValueToType(value: $0) }

self.overrides = overrides ?? [:]
} catch {
return nil
}
self.key = key
self.typeHint = typeHint
self.associatedProperty = dict["associatedProperty"] as? String
self.type = PropertyType(rawValue: typeHint)
self.defaultValue = defaultValue
if let overrides = dict["overrides"] as? [String: T] {
self.overrides = overrides
} else {
self.overrides = [:]
}
self.description = dict["description"] as? String
}

func value(for scheme: String) -> T {
Expand Down
15 changes: 10 additions & 5 deletions Sources/Config/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func byteArrayOutput(from: [UInt8]) -> String {

enum PropertyType: String {
case string = "String"
case optionalString = "String?"
case url = "URL"
case encrypted = "Encrypted"
case encryptionKey = "EncryptionKey"
Expand Down Expand Up @@ -85,15 +86,19 @@ enum PropertyType: String {
}

func valueDeclaration(for value: Any, iv: IV, key: String?) -> String {
let stringValue = { () -> String in
if let string = value as? String, string.isEmpty {
return "\"\""
let stringValueAllowingOptional = { (optional: Bool) -> String in
if let string = value as? String {
return string.isEmpty ? "\"\"" : "#\"\(string)\"#"
} else if optional {
return "nil"
}
return "#\"\(value)\"#"
}
switch self {
case .string:
return stringValue()
return stringValueAllowingOptional(false)
case .optionalString:
return stringValueAllowingOptional(true)
case .url:
return #"URL(string: "\#(value)")!"#
case .encryptionKey:
Expand All @@ -113,7 +118,7 @@ enum PropertyType: String {
case .bool:
return "\(value as! Bool)"
case .regex:
return "try! NSRegularExpression(pattern: \(stringValue()), options: [])"
return "try! NSRegularExpression(pattern: \(stringValueAllowingOptional(false)), options: [])"
default:
return "\(value)"
}
Expand Down
32 changes: 31 additions & 1 deletion Tests/ConfigTests/ConfigurationPropertyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,39 @@ class ConfigurationPropertyTests: XCTestCase {
func testItCanWriteARegexProperty() throws {
let imageProperty = ConfigurationProperty<String>(key: "test", typeHint: "Regex", dict: [
"defaultValue": "an\\sexpression",
])
])
let expectedValue = ##" static let test: NSRegularExpression = try! NSRegularExpression(pattern: #"an\sexpression"#, options: [])"##
let actualValue = try whenTheDeclarationIsWritten(for: imageProperty)
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteAnOptionalStringProperty() throws {
let property = ConfigurationProperty<String?>(key: "test", typeHint: "String?", dict: [
"defaultValue": NSNull.self
])
let expectedValue = " static let test: String? = nil"
let actualValue = try whenTheDeclarationIsWritten(for: property)
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteAnOptionalOverrideStringProperty() throws {
let property = ConfigurationProperty<String?>(key: "test", typeHint: "String?", dict: [
"defaultValue": "Test",
"overrides": [
"bla": NSNull.self
]
])
let expectedValue = " static let test: String? = nil"
let actualValue = try whenTheDeclarationIsWritten(for: property, scheme: "bla")
expect(actualValue).to(equal(expectedValue))
}

func testItCanWriteAnOptionalStringPropertyWithAValue() throws {
let property = ConfigurationProperty<String?>(key: "test", typeHint: "String?", dict: [
"defaultValue": "Test"
])
let expectedValue = ##" static let test: String? = #"Test"#"##
let actualValue = try whenTheDeclarationIsWritten(for: property)
expect(actualValue).to(equal(expectedValue))
}
}
10 changes: 8 additions & 2 deletions Tests/ConfigTests/CustomTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ class CustomTypeTests: XCTestCase {
expect(type?.placeholders.last?.name).to(equal("secondplaceholder"))
expect(type?.placeholders.last?.type).to(equal(.bool))
}

func testItCanParsePlaceholdersWithAnOptionalStringAsATypeAttribute() {
let type = CustomType(source: givenATypeDictionaryWithTypeAnnotations(firstPlaceholderType: "String?"))
expect(type?.placeholders.first?.name).to(equal("firstplaceholder"))
expect(type?.placeholders.first?.type).to(equal(.optionalString))
}
}

func givenATypeDictionary() -> [String: Any] {
Expand All @@ -71,9 +77,9 @@ func givenATypeDictionary() -> [String: Any] {
]
}

func givenATypeDictionaryWithTypeAnnotations() -> [String: Any] {
func givenATypeDictionaryWithTypeAnnotations(firstPlaceholderType: String = "String", secondPlaceholderType: String = "Bool") -> [String: Any] {
return [
"typeName": "CustomType",
"initialiser": "CustomType(oneThing: {firstplaceholder:String}, secondThing: {secondplaceholder:Bool})"
"initialiser": "CustomType(oneThing: {firstplaceholder:\(firstPlaceholderType)}, secondThing: {secondplaceholder:\(secondPlaceholderType)})"
]
}

0 comments on commit 301123d

Please sign in to comment.