diff --git a/CHANGELOG.md b/CHANGELOG.md index c890c5da..acbfff7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,10 @@ _None_ ### New Features -_None_ +* Added `Parameters.flatten(dictionary:)` method to do the opposite of + `Parameters.parse(items:)` and turn a dictionary into the list of parameters to pass from the command line. + [Olivier Halligon](https://github.com/AliSoftware) + [#70](https://github.com/SwiftGen/StencilSwiftKit/pull/70) ### Internal Changes diff --git a/Sources/Filters+Numbers.swift b/Sources/Filters+Numbers.swift index e866ee9c..95b86714 100644 --- a/Sources/Filters+Numbers.swift +++ b/Sources/Filters+Numbers.swift @@ -11,7 +11,7 @@ extension Filters { enum Numbers { static func hexToInt(_ value: Any?) throws -> Any? { guard let value = value as? String else { throw Filters.Error.invalidInputType } - return Int(value, radix: 16) + return Int(value, radix: 16) } static func int255toFloat(_ value: Any?) throws -> Any? { diff --git a/Sources/Parameters.swift b/Sources/Parameters.swift index 90e4fe30..be0834fa 100644 --- a/Sources/Parameters.swift +++ b/Sources/Parameters.swift @@ -37,6 +37,23 @@ public enum Parameters { } } + /// Flatten a dictionary into a list of "key.path=value" pairs. + /// This method recursively visits the object to build its flat representation. + /// + /// - Parameters: + /// - dictionary: The dictionary to recursively flatten into key pairs + /// - Returns: The list of flatten "key.path=value" pair representations of the object + /// + /// - Note: flatten is the counterpart of parse. flatten(parse(x)) == parse(flatten(x)) == x + /// + /// - Example: + /// + /// flatten(["a":["b":1,"c":[2,3]]]) + /// // ["a.b=1","a.c=2","a.c=3"] + public static func flatten(dictionary: StringDict) -> [String] { + return flatten(object: dictionary, keyPrefix: "") + } + // MARK: - Private methods /// Parse a single `key=value` (or `key`) string and inserts it into @@ -115,4 +132,32 @@ public enum Parameters { throw Error.invalidSyntax(value: string) } } + + /// Flatten an object (dictionary, array or single object) into a list of keypath-type k=v pairs. + /// This method recursively visits the object to build the flat representation. + /// + /// - Parameters: + /// - object: The object to recursively flatten + /// - keyPrefix: The prefix to use when creating keys. + /// This is used to build the keyPath via recusrive calls of this function. + /// You should start the root call of this recursive function with an empty keyPrefix. + /// - Returns: The list of flatten "key.path=value" pair representations of the object + private static func flatten(object: Any, keyPrefix: String = "") -> [String] { + var values: [String] = [] + switch object { + case is String, is Int, is Double: + values.append("\(keyPrefix)=\(object)") + case is Bool: + values.append(keyPrefix) + case let dict as [String: Any]: + for (key, value) in dict { + let fullKey = keyPrefix.isEmpty ? key : "\(keyPrefix).\(key)" + values += flatten(object: value, keyPrefix: fullKey) + } + case let array as [Any]: + values += array.flatMap { flatten(object: $0, keyPrefix: keyPrefix) } + default: break + } + return values + } } diff --git a/Tests/StencilSwiftKitTests/ContextTests.swift b/Tests/StencilSwiftKitTests/ContextTests.swift index a80cf65c..b972db31 100644 --- a/Tests/StencilSwiftKitTests/ContextTests.swift +++ b/Tests/StencilSwiftKitTests/ContextTests.swift @@ -32,7 +32,7 @@ class ContextTests: XCTestCase { } func testWithContext() throws { - let context: [String : Any] = ["foo": "bar", "hello": true] + let context: [String: Any] = ["foo": "bar", "hello": true] let result = try StencilContext.enrich(context: context, parameters: [], diff --git a/Tests/StencilSwiftKitTests/ParametersTests.swift b/Tests/StencilSwiftKitTests/ParametersTests.swift index 20167762..f0717dbb 100644 --- a/Tests/StencilSwiftKitTests/ParametersTests.swift +++ b/Tests/StencilSwiftKitTests/ParametersTests.swift @@ -17,6 +17,13 @@ class ParametersTests: XCTestCase { XCTAssertEqual(result["b"] as? String, "hello") XCTAssertEqual(result["c"] as? String, "x=y") XCTAssertEqual(result["d"] as? Bool, true) + + // Test the opposite operation (flatten) as well + let reverse = Parameters.flatten(dictionary: result) + XCTAssertEqual(reverse.count, items.count, + "Flattening the resulting dictionary back did not result in the equivalent of the original list") + XCTAssertEqual(Set(reverse), Set(items), + "Flattening the resulting dictionary back did not result in the equivalent of the original list") } func testStructured() throws { @@ -28,6 +35,13 @@ class ParametersTests: XCTestCase { XCTAssertEqual(sub["baz"] as? String, "1") XCTAssertEqual(sub["bar"] as? String, "2") XCTAssertEqual(sub["test"] as? Bool, true) + + // Test the opposite operation (flatten) as well + let reverse = Parameters.flatten(dictionary: result) + XCTAssertEqual(reverse.count, items.count, + "Flattening the resulting dictionary back did not result in the equivalent of the original list") + XCTAssertEqual(Set(reverse), Set(items), + "Flattening the resulting dictionary back did not result in the equivalent of the original list") } func testDeepStructured() throws { @@ -40,6 +54,13 @@ class ParametersTests: XCTestCase { guard let baz = bar["baz"] as? [String: Any] else { XCTFail("Parsed parameter should be a dictionary"); return } guard let qux = baz["qux"] as? String else { XCTFail("Parsed parameter should be a string"); return } XCTAssertEqual(qux, "1") + + // Test the opposite operation (flatten) as well + let reverse = Parameters.flatten(dictionary: result) + XCTAssertEqual(reverse.count, items.count, + "Flattening the resulting dictionary back did not result in the equivalent of the original list") + XCTAssertEqual(Set(reverse), Set(items), + "Flattening the resulting dictionary back did not result in the equivalent of the original list") } func testRepeated() throws { @@ -52,9 +73,18 @@ class ParametersTests: XCTestCase { XCTAssertEqual(sub[0], "1") XCTAssertEqual(sub[1], "2") XCTAssertEqual(sub[2], "hello") + + // Test the opposite operation (flatten) as well + let reverse = Parameters.flatten(dictionary: result) + XCTAssertEqual(reverse.count, items.count, + "Flattening the resulting dictionary back did not result in the equivalent of the original list") + XCTAssertEqual(Set(reverse), Set(items), + "Flattening the resulting dictionary back did not result in the equivalent of the original list") + XCTAssertEqual(reverse, items, + "The order of arrays are properly preserved when flattened") } - func testInvalidSyntax() { + func testParseInvalidSyntax() { // invalid character do { let items = ["foo:1"] @@ -89,7 +119,7 @@ class ParametersTests: XCTestCase { } } - func testInvalidKey() { + func testParseInvalidKey() { // key may only be alphanumeric or '.' do { let items = ["foo:bar=1"] @@ -124,7 +154,7 @@ class ParametersTests: XCTestCase { } } - func testInvalidStructure() { + func testParseInvalidStructure() { // can't switch from string to dictionary do { let items = ["foo=1", "foo.bar=1"]