diff --git a/Sources/Parameters.swift b/Sources/Parameters.swift index 8ccd5ef9..2a3ce9cf 100644 --- a/Sources/Parameters.swift +++ b/Sources/Parameters.swift @@ -7,53 +7,60 @@ import Foundation public enum ParametersError: Error { - case invalidSyntax(value: String) - case invalidKey(key: String, value: String) + case invalidSyntax(value: String) + case invalidKey(key: String, value: String) } public enum Parameters { - typealias Parameter = (key: String, value: String) - public typealias StringDict = [String: Any] - - public static func parse(items: [String]) throws -> StringDict { - let parameters: [Parameter] = try items.map { item in - let parts = item.components(separatedBy: "=") - guard parts.count == 2 else { throw ParametersError.invalidSyntax(value: item) } - return (key: parts[0], value: parts[1]) - } - - var result = StringDict() - for parameter in parameters { - result = try parse(item: parameter, result: result) - } - - return result - } - - private static func parse(item: Parameter, result: StringDict) throws -> StringDict { - let parts = item.key.components(separatedBy: ".") - var result = result - - // no sub keys, may need to convert to array if repeat key - if parts.count == 1 { - if let current = result[item.key] as? [String] { - result[item.key] = current + [item.value] - } else if let current = result[item.key] { - result[item.key] = [current, item.value] - } else { - result[item.key] = item.value - } - } else if parts.count > 1 { - // recurse into sub keys - let part = parts.first ?? "" - let sub = (key: parts.suffix(from: 1).joined(separator: "."), value: item.value) - let current = result[part] as? StringDict ?? StringDict() - - result[part] = try parse(item: sub, result: current) - } else { - throw ParametersError.invalidKey(key: item.key, value: item.value) - } - - return result - } + typealias Parameter = (key: String, value: String) + public typealias StringDict = [String: Any] + + public static func parse(items: [String]) throws -> StringDict { + let parameters: [Parameter] = try items.map { item in + let parts = item.components(separatedBy: "=") + guard parts.count == 2 else { throw ParametersError.invalidSyntax(value: item) } + return (key: parts[0], value: parts[1]) + } + + var result = StringDict() + for parameter in parameters { + result = try parse(item: parameter, result: result) + } + + return result + } + + private static func parse(item: Parameter, result: StringDict) throws -> StringDict { + let parts = item.key.components(separatedBy: ".") + let key = parts.first ?? "" + var result = result + + // validate key + guard validate(key: key) else { throw ParametersError.invalidKey(key: item.key, value: item.value) } + + // no sub keys, may need to convert to array if repeat key + if parts.count == 1 { + if let current = result[key] as? [String] { + result[key] = current + [item.value] + } else if let current = result[item.key] { + result[key] = [current, item.value] + } else { + result[key] = item.value + } + } else if parts.count > 1 { + // recurse into sub keys + let sub = (key: parts.suffix(from: 1).joined(separator: "."), value: item.value) + let current = result[key] as? StringDict ?? StringDict() + + result[key] = try parse(item: sub, result: current) + } + + return result + } + + // a valid key is not empty and only alphanumerical + private static func validate(key: String) -> Bool { + return !key.isEmpty && + key.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil + } } diff --git a/Tests/TestSuites/ParametersTests.swift b/Tests/TestSuites/ParametersTests.swift index a512b29b..1379e693 100644 --- a/Tests/TestSuites/ParametersTests.swift +++ b/Tests/TestSuites/ParametersTests.swift @@ -22,20 +22,90 @@ class ParametersTests: XCTestCase { let result = try! Parameters.parse(items: items) XCTAssertEqual(result.count, 1, "1 parameter should have been parsed") - guard let sub = result["foo"] as? [String: String] else { XCTFail("Parsed parameter is a dictionary"); return } + guard let sub = result["foo"] as? [String: String] else { XCTFail("Parsed parameter should be a dictionary"); return } XCTAssertEqual(sub["baz"], "1") XCTAssertEqual(sub["bar"], "2") } + func testDeepStructured() { + let items = ["foo.bar.baz.qux=1"] + let result = try! Parameters.parse(items: items) + + XCTAssertEqual(result.count, 1, "1 parameter should have been parsed") + guard let foo = result["foo"] as? [String: Any] else { XCTFail("Parsed parameter should be a dictionary"); return } + guard let bar = foo["bar"] as? [String: Any] else { XCTFail("Parsed parameter should be a dictionary"); return } + 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") + } + func testRepeated() { let items = ["foo=1", "foo=2", "foo=hello"] let result = try! Parameters.parse(items: items) XCTAssertEqual(result.count, 1, "1 parameter should have been parsed") - guard let sub = result["foo"] as? [String] else { XCTFail("Parsed parameter is an array"); return } + guard let sub = result["foo"] as? [String] else { XCTFail("Parsed parameter should be an array"); return } XCTAssertEqual(sub.count, 3, "Array has 3 elements") XCTAssertEqual(sub[0], "1") XCTAssertEqual(sub[1], "2") XCTAssertEqual(sub[2], "hello") } + + func testInvalidSyntax() { + do { + let items = ["foo:1"] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidSyntax { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + + do { + let items = ["foo!1"] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidSyntax { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + + do { + let items = [""] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidSyntax { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + } + + func testInvalidKey() { + do { + let items = ["foo:bar=1"] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidKey { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + + do { + let items = [".=1"] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidKey { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + + do { + let items = ["foo.=1"] + _ = try Parameters.parse(items: items) + } catch ParametersError.invalidKey { + // That's the expected exception we want to happen + } catch let error { + XCTFail("Unexpected error occured while parsing: \(error)") + } + } }