diff --git a/GenericJSON/Querying.swift b/GenericJSON/Querying.swift index 7566b1f..91605f5 100644 --- a/GenericJSON/Querying.swift +++ b/GenericJSON/Querying.swift @@ -4,42 +4,87 @@ public extension JSON { /// Return the string value if this is a `.string`, otherwise `nil` var stringValue: String? { - if case .string(let value) = self { - return value + get { + if case .string(let value) = self { + return value + } + return nil + } + set { + if let newValue = newValue { + self = .string(newValue) + } else { + self = .null + } } - return nil } /// Return the double value if this is a `.number`, otherwise `nil` var doubleValue: Double? { - if case .number(let value) = self { - return value + get { + if case .number(let value) = self { + return value + } + return nil + } + set { + if let newValue = newValue { + self = .number(newValue) + } else { + self = .null + } } - return nil } /// Return the bool value if this is a `.bool`, otherwise `nil` var boolValue: Bool? { - if case .bool(let value) = self { - return value + get { + if case .bool(let value) = self { + return value + } + return nil + } + set { + if let newValue = newValue { + self = .bool(newValue) + } else { + self = .null + } } - return nil } /// Return the object value if this is an `.object`, otherwise `nil` var objectValue: [String: JSON]? { - if case .object(let value) = self { - return value + get { + if case .object(let value) = self { + return value + } + return nil + } + set { + if let newValue = newValue { + self = .object(newValue) + } else { + self = .null + } } - return nil } /// Return the array value if this is an `.array`, otherwise `nil` var arrayValue: [JSON]? { - if case .array(let value) = self { - return value + get { + if case .array(let value) = self { + return value + } + return nil + } + set { + if let newValue = newValue { + self = .array(newValue) + } else { + self = .null + } } - return nil } /// Return `true` iff this is `.null` @@ -50,36 +95,62 @@ public extension JSON { return false } + mutating func setToNull() { + self = .null + } + /// If this is an `.array`, return item at index /// /// If this is not an `.array` or the index is out of bounds, returns `nil`. subscript(index: Int) -> JSON? { - if case .array(let arr) = self, arr.indices.contains(index) { - return arr[index] + get { + if case .array(let arr) = self, arr.indices.contains(index) { + return arr[index] + } + return nil + } + set { + guard case .array(var arr) = self else { + fatalError("Subscript assignment of JSON value that is not an array") + } + + arr[index] = newValue! + self = .array(arr) } - return nil } /// If this is an `.object`, return item at key subscript(key: String) -> JSON? { - if case .object(let dict) = self { - return dict[key] + get { + if case .object(let dict) = self { + return dict[key] + } + return nil + } + set { + guard case .object(var dict) = self else { + fatalError("Subscript assignment of JSON value that is not a dictionary") + } + + dict[key] = newValue! + self = .object(dict) } - return nil } /// Dynamic member lookup sugar for string subscripts /// /// This lets you write `json.foo` instead of `json["foo"]`. subscript(dynamicMember member: String) -> JSON? { - return self[member] + get { self[member] } + set { self[member] = newValue } } /// Return the JSON type at the keypath if this is an `.object`, otherwise `nil` /// /// This lets you write `json[keyPath: "foo.bar.jar"]`. subscript(keyPath keyPath: String) -> JSON? { - return queryKeyPath(keyPath.components(separatedBy: ".")) + get { queryKeyPath(keyPath.components(separatedBy: ".")) } + set { updateKeyPath(keyPath.components(separatedBy: "."), &self, newValue!) } } func queryKeyPath(_ path: T) -> JSON? where T: Collection, T.Element == String { @@ -103,5 +174,29 @@ public extension JSON { return tail.isEmpty ? value : value.queryKeyPath(tail) } - + + func updateKeyPath(_ path: T, _ json: inout JSON, _ newValue: JSON) where T: Collection, T.Element == String { + // Only object values may be subscripted + guard case .object = json else { + fatalError("Keypath \(path) applied to non-object") + } + + // Is the path non-empty? + guard let head = path.first else { + return + } + + let tail = path.dropFirst() + + guard !tail.isEmpty else { + json[head] = newValue + return + } + + if json[head] == nil { + json[head] = [:] + } + + updateKeyPath(tail, &json[head]!, newValue) + } } diff --git a/GenericJSONTests/CodingTests.swift b/GenericJSONTests/CodingTests.swift index 11ddfb0..3ebb768 100644 --- a/GenericJSONTests/CodingTests.swift +++ b/GenericJSONTests/CodingTests.swift @@ -22,13 +22,6 @@ class CodingTests: XCTestCase { """) } - func testFragmentEncoding() { - let fragments: [JSON] = ["foo", 1, true, nil] - for f in fragments { - XCTAssertThrowsError(try JSONEncoder().encode(f)) - } - } - func testDecoding() throws { let input = """ {"array":[1],"num":1,"bool":true,"obj":{},"null":null,"str":"baz"} diff --git a/GenericJSONTests/InitializationTests.swift b/GenericJSONTests/InitializationTests.swift index 9f46a85..4961a9d 100644 --- a/GenericJSONTests/InitializationTests.swift +++ b/GenericJSONTests/InitializationTests.swift @@ -43,8 +43,8 @@ class InitializationTests: XCTestCase { func testInitializationFromCodable() throws { struct Foo: Codable { - let a: String = "foo" - let b: Bool = true + var a: String = "foo" + var b: Bool = true } let json = try JSON(encodable: Foo()) diff --git a/GenericJSONTests/QueryingTests.swift b/GenericJSONTests/QueryingTests.swift index 7a5d6e5..cfd7d4b 100644 --- a/GenericJSONTests/QueryingTests.swift +++ b/GenericJSONTests/QueryingTests.swift @@ -3,42 +3,79 @@ import GenericJSON class QueryingTests: XCTestCase { - func testStringValue() { + func testGetStringValue() { XCTAssertEqual(JSON.string("foo").stringValue, "foo") XCTAssertEqual(JSON.number(42).stringValue, nil) XCTAssertEqual(JSON.null.stringValue, nil) } - func testFloatValue() { + func testSetStringValue() { + var value: JSON = "foo" + value = "bar" + XCTAssertEqual(value.stringValue, "bar") + } + + func testGetFloatValue() { XCTAssertEqual(JSON.number(42).doubleValue, 42) XCTAssertEqual(JSON.string("foo").doubleValue, nil) XCTAssertEqual(JSON.null.doubleValue, nil) } - func testBoolValue() { + func testSetFloatValue() { + var value: JSON = 42 + value = 43 + XCTAssertEqual(value.doubleValue, 43) + } + + func testGetBoolValue() { XCTAssertEqual(JSON.bool(true).boolValue, true) XCTAssertEqual(JSON.string("foo").boolValue, nil) XCTAssertEqual(JSON.null.boolValue, nil) } - func testObjectValue() { + func testSetBoolValue() { + var value: JSON = true + value = false + XCTAssertEqual(value, false) + } + + func testGetObjectValue() { XCTAssertEqual(JSON.object(["foo": "bar"]).objectValue, ["foo": JSON.string("bar")]) XCTAssertEqual(JSON.string("foo").objectValue, nil) XCTAssertEqual(JSON.null.objectValue, nil) } - func testArrayValue() { + func testSetObjectValue() { + var value: JSON = ["foo": "bar"] + value = ["foo": "baz"] + value["new"] = 42 + XCTAssertEqual(value, ["foo": "baz", "new": JSON(42)]) + } + + func testGetArrayValue() { XCTAssertEqual(JSON.array(["foo", "bar"]).arrayValue, [JSON.string("foo"), JSON.string("bar")]) XCTAssertEqual(JSON.string("foo").arrayValue, nil) XCTAssertEqual(JSON.null.arrayValue, nil) } - func testNullValue() { + func testSetArrayValue() { + var value: JSON = ["foo", "bar"] + value = ["baz"] + XCTAssertEqual(value, ["baz"]) + } + + func testGetNullValue() { XCTAssertEqual(JSON.null.isNull, true) XCTAssertEqual(JSON.string("foo").isNull, false) } - func testArraySubscripting() { + func testSetNullValue() { + var value: JSON = "foo" + value = nil + XCTAssertEqual(value, JSON.null) + } + + func testGetArraySubscripting() { let json: JSON = ["foo", "bar"] XCTAssertEqual(json[0], JSON.string("foo")) XCTAssertEqual(json[-1], nil) @@ -46,20 +83,50 @@ class QueryingTests: XCTestCase { XCTAssertEqual(json["foo"], nil) } - func testStringSubscripting() { + func testSetArraySubscripting() { + var value: JSON = ["foo", "bar"] + value[1] = "baz" + XCTAssertEqual(value, ["foo", "baz"]) + } + + func testGetStringSubscripting() { let json: JSON = ["foo": "bar"] XCTAssertEqual(json["foo"], JSON.string("bar")) XCTAssertEqual(json[0], nil) XCTAssertEqual(json["nonesuch"], nil) } - func testStringSubscriptingSugar() { + func testSetStringSubscripting() { + var json: JSON = ["foo": "bar"] + json["foo"] = "baz" + XCTAssertEqual(json, ["foo": "baz"]) + } + + func testSetStringSubscriptingNewValue() { + var json: JSON = ["foo": "bar"] + json["baz"] = 42 + XCTAssertEqual(json, ["foo": "bar", "baz": JSON(42)]) + } + + func testGetStringSubscriptingSugar() { let json: JSON = ["foo": "bar"] XCTAssertEqual(json.foo, JSON.string("bar")) XCTAssertEqual(json.nonesuch, nil) } + + func testSetStringSubscriptingSugar() { + var json: JSON = ["foo": "bar"] + json.foo = "baz" + XCTAssertEqual(json, ["foo": "baz"]) + } + + func testSetStringSubscriptingSugarNewValue() { + var json: JSON = ["foo": "bar"] + json.baz = 42 + XCTAssertEqual(json, ["foo": "bar", "baz": JSON(42)]) + } - func testKeyPath() { + func testGetKeyPath() { let json: JSON = [ "string": "foo bar", "boolean": true, @@ -81,4 +148,48 @@ class QueryingTests: XCTestCase { XCTAssertEqual(json[keyPath: "object.arr"], [1, 2, 3]) XCTAssertEqual(json[keyPath: "object.obj.y"], "tar") } + + func testSetKeyPath() { + var json: JSON = [ + "string": "foo bar", + "boolean": true, + "number": 123, + "object": [ + "str": "col", + "arr": [1, 2, 3], + "obj": [ + "x": "rah", + "y": "tar", + "z": "yaz" + ] + ] + ] + + json[keyPath: "boolean"] = false + XCTAssertEqual(json["boolean"], false) + + json[keyPath: "string"] = "baz" + XCTAssertEqual(json.string, "baz") + + json[keyPath: "number"] = 42 + XCTAssertEqual(json["number"], 42) + + json[keyPath: "object.str"] = "new" + XCTAssertEqual(json["object"]?["str"], "new") + + json[keyPath: "object.arr"] = [42] + XCTAssertEqual(json["object"]?["arr"], [42]) + + json[keyPath: "object.obj.y"] = "new" + XCTAssertEqual(json["object"]?["obj"]?["y"], "new") + } + + func testSetKeyPathNewValue() { + var json: JSON = [ + "object": [:] + ] + + json[keyPath: "object.new.new"] = 42 + XCTAssertEqual(json["object"]?["new"]?["new"], 42) + } }