Skip to content

Support mutating JSON values #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 119 additions & 24 deletions GenericJSON/Querying.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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<T>(_ path: T) -> JSON? where T: Collection, T.Element == String {
Expand All @@ -103,5 +174,29 @@ public extension JSON {

return tail.isEmpty ? value : value.queryKeyPath(tail)
}


func updateKeyPath<T>(_ 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)
}
}
7 changes: 0 additions & 7 deletions GenericJSONTests/CodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 2 additions & 2 deletions GenericJSONTests/InitializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading