Skip to content

Commit

Permalink
better key validation + more extensive tests
Browse files Browse the repository at this point in the history
  • Loading branch information
djbe committed Jan 28, 2017
1 parent 245fca3 commit 656b1c9
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 48 deletions.
99 changes: 53 additions & 46 deletions Sources/Parameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
74 changes: 72 additions & 2 deletions Tests/TestSuites/ParametersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}

0 comments on commit 656b1c9

Please sign in to comment.