Skip to content
Merged
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
109 changes: 99 additions & 10 deletions json.carp
Original file line number Diff line number Diff line change
Expand Up @@ -679,15 +679,27 @@ array or the index is out of bounds. Returns a copy of the value.")
(Maybe.Nothing) (Maybe.Nothing))
_ (Maybe.Nothing)))

(doc get-in "performs nested key lookup through objects. Returns a copy.
(private get-step)
(hidden get-step)
(defn get-step [j k]
(match-ref j
(JSON.Arr _)
(match (Int.from-string k)
(Maybe.Just i) (if (>= i 0) (JSON.nth j i) (Maybe.Nothing))
(Maybe.Nothing) (Maybe.Nothing))
_ (JSON.get j k)))

```
(JSON.get-in &j &[@\"data\" @\"users\"])
```")
(doc get-in "performs nested lookup through objects and arrays. String path
elements that parse as non-negative integers index into arrays; all other
elements are used as object keys. Returns a copy.

(JSON.get-in &j &[@\"data\" @\"users\" @\"0\" @\"name\"])")
(defn get-in [j keys]
(Array.reduce
&(fn [mj k]
(match mj (Maybe.Nothing) (Maybe.Nothing) (Maybe.Just j) (JSON.get &j k)))
(match mj
(Maybe.Nothing) (Maybe.Nothing)
(Maybe.Just j) (JSON.get-step &j k)))
(Maybe.Just @j)
keys))

Expand All @@ -697,16 +709,93 @@ automatically boxed.")

(doc obj "builds a JSON object from an array of key-value pairs.

```
(JSON.obj [(JSON.entry @\"a\" (JSON.Num 1.0))
(JSON.entry @\"b\" (JSON.Bool true))])
```")
(JSON.obj [(JSON.entry @\"a\" (JSON.Num 1.0))
(JSON.entry @\"b\" (JSON.Bool true))])")
(defn obj [entries]
(JSON.Obj
(Array.reduce
&(fn [m p] (Map.put m (Pair.a p) (Pair.b p)))
(the (Map String (Box JSON)) {})
&entries))))
&entries)))

(doc set-key "sets a key in a JSON object, replacing any existing value.
If the value is not an object, returns it unchanged.")
(defn set-key [j k v]
(match j
(JSON.Obj m) (let [bv (Box.init v)] (JSON.Obj (Map.put m k &bv)))
other (do (ignore v) other)))

(doc delete-key "removes a key from a JSON object.
If the value is not an object or the key is absent, returns it unchanged.")
(defn delete-key [j k]
(match j (JSON.Obj m) (JSON.Obj (Map.remove m k)) other other))

(doc push "appends a value to a JSON array.
If the value is not an array, returns it unchanged.")
(defn push [j v]
(match j
(JSON.Arr a) (JSON.Arr (Array.push-back a (Box.init v)))
other (do (ignore v) other)))

(doc set-nth "replaces the value at index `i` in a JSON array.
If the value is not an array or the index is out of bounds, returns it
unchanged.")
(defn set-nth [j i v]
(match j
(JSON.Arr a)
(if (and (>= i 0) (< i (Array.length &a)))
(JSON.Arr (Array.aset a i (Box.init v)))
(do (ignore v) (JSON.Arr a)))
other (do (ignore v) other)))

(private set-in-at)
(hidden set-in-at)
(register set-in-at (Fn [JSON (Ref (Array String)) Int JSON] JSON))
(defn set-in-at [j keys idx v]
(if (>= idx (Array.length keys))
(do (ignore j) v)
(let [k (Array.unsafe-nth keys idx)]
(match j
(JSON.Arr a)
(match (Int.from-string k)
(Maybe.Just i)
(if (and (>= i 0) (< i (Array.length &a)))
(let [child (Box.unbox @(Array.unsafe-nth &a i))
updated (JSON.set-in-at child keys (Int.inc idx) v)]
(JSON.Arr (Array.aset a i (Box.init updated))))
(do (ignore v) (JSON.Arr a)))
(Maybe.Nothing) (do (ignore v) (JSON.Arr a)))
(JSON.Obj m)
(let [child (match (Map.get-maybe &m k)
(Maybe.Just bx) (Box.unbox bx)
(Maybe.Nothing) (JSON.Obj (the (Map String (Box JSON)) {})))
updated (JSON.set-in-at child keys (Int.inc idx) v)
bv (Box.init updated)]
(JSON.Obj (Map.put m k &bv)))
other (do (ignore v) other)))))

(doc set-in "sets a value at a nested path through objects and arrays.

Path elements that parse as non-negative integers index into arrays;
all other elements are used as object keys. Missing intermediate object
keys are created automatically.

(JSON.set-in j &[@\"users\" @\"0\" @\"name\"] (JSON.Str @\"Alice\"))")
(defn set-in [j keys v] (JSON.set-in-at j keys 0 v))

(doc update-in "applies a function to the value at a nested path.

Path elements that parse as non-negative integers index into arrays.
If the path does not resolve to a value, returns the input unchanged.

(JSON.update-in j &[@\"count\"] (fn [n]
(match (JSON.as-num &n)
(Maybe.Just x) (JSON.Num (+ x 1.0))
(Maybe.Nothing) n)))")
(defn update-in [j keys f]
(match (JSON.get-in &j keys)
(Maybe.Just old) (JSON.set-in j keys (f old))
(Maybe.Nothing) (do (ignore f) j))))

(doc to-json "converts a Carp value to its JSON representation.")
(definterface to-json (Fn [a] JSON))
Expand Down
226 changes: 225 additions & 1 deletion test/json.carp
Original file line number Diff line number Diff line change
Expand Up @@ -920,4 +920,228 @@ break")) "str string with newline")
(Result.Success j2) (= &(ser &j) &(ser &j2))
_ false)
_ false))
"pretty-str output parses back to equivalent structure"))
"pretty-str output parses back to equivalent structure")

(assert-equal test
"{\"a\":1,\"b\":2}"
&(ser
&(JSON.set-key
(JSON.obj [(JSON.entry @"a" (JSON.Num 1.0))])
"b"
(JSON.Num 2.0)))
"set-key adds a new key")

(assert-equal test
"{\"a\":99}"
&(ser
&(JSON.set-key
(JSON.obj [(JSON.entry @"a" (JSON.Num 1.0))])
"a"
(JSON.Num 99.0)))
"set-key replaces an existing key")

(assert-equal test
"null"
&(ser &(JSON.set-key (JSON.Null) "x" (JSON.Num 1.0)))
"set-key on non-object returns unchanged")

(assert-equal test
"{}"
&(ser &(JSON.delete-key (JSON.obj [(JSON.entry @"a" (JSON.Num 1.0))]) "a"))
"delete-key removes existing key")

(assert-true test
(let [j (JSON.obj [(JSON.entry @"a" (JSON.Num 1.0))])]
(match (JSON.get &(JSON.delete-key j "z") "a") (Maybe.Just _) true _ false))
"delete-key on missing key returns unchanged")

(assert-equal test
"[1]"
&(ser &(JSON.delete-key (JSON.Arr [(Box.init (JSON.Num 1.0))]) "x"))
"delete-key on non-object returns unchanged")

(assert-equal test
"[1,2,3]"
&(ser
&(JSON.push
(JSON.Arr [(Box.init (JSON.Num 1.0)) (Box.init (JSON.Num 2.0))])
(JSON.Num 3.0)))
"push appends to array")

(assert-equal test
"[1]"
&(ser &(JSON.push (JSON.Arr (the (Array (Box JSON)) [])) (JSON.Num 1.0)))
"push to empty array")

(assert-equal test
"null"
&(ser &(JSON.push (JSON.Null) (JSON.Num 1.0)))
"push on non-array returns unchanged")

(assert-equal test
"[1,99,3]"
&(ser
&(JSON.set-nth
(JSON.Arr
[(Box.init (JSON.Num 1.0))
(Box.init (JSON.Num 2.0))
(Box.init (JSON.Num 3.0))])
1
(JSON.Num 99.0)))
"set-nth replaces element at index")

(assert-equal test
"[99]"
&(ser
&(JSON.set-nth (JSON.Arr [(Box.init (JSON.Num 1.0))]) 0 (JSON.Num 99.0)))
"set-nth replaces first element")

(assert-equal test
"[1,2]"
&(ser
&(JSON.set-nth
(JSON.Arr [(Box.init (JSON.Num 1.0)) (Box.init (JSON.Num 2.0))])
5
(JSON.Num 99.0)))
"set-nth out of bounds returns unchanged")

(assert-equal test
"null"
&(ser &(JSON.set-nth (JSON.Null) 0 (JSON.Num 1.0)))
"set-nth on non-array returns unchanged")

(assert-true test
(let [j (JSON.Arr [(Box.init (JSON.Str @"a")) (Box.init (JSON.Str @"b"))])]
(match (JSON.get-in &j &[@"1"])
(Maybe.Just v) (= &(ser &v) "\"b\"")
_ false))
"get-in indexes into array with numeric string")

(assert-true test
(match (JSON.parse "{\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]}")
(Result.Success j)
(match (JSON.get-in &j &[@"users" @"1" @"name"])
(Maybe.Just v) (= &(ser &v) "\"Bob\"")
_ false)
_ false)
"get-in traverses mixed objects and arrays")

(assert-true test
(let [j (JSON.Arr [(Box.init (JSON.Num 1.0))])]
(Maybe.nothing? &(JSON.get-in &j &[@"5"])))
"get-in returns Nothing for out-of-bounds array index")

(assert-true test
(let [j (JSON.Arr [(Box.init (JSON.Num 1.0))])]
(Maybe.nothing? &(JSON.get-in &j &[@"abc"])))
"get-in returns Nothing for non-numeric key on array")

(assert-equal test
"{\"a\":99}"
&(ser
&(JSON.set-in
(JSON.obj [(JSON.entry @"a" (JSON.Num 1.0))])
&[@"a"]
(JSON.Num 99.0)))
"set-in replaces value at single key")

(assert-true test
(match (JSON.parse "{\"a\":{\"b\":1}}")
(Result.Success j)
(let [updated (JSON.set-in j &[@"a" @"b"] (JSON.Num 99.0))]
(match (JSON.get-in &updated &[@"a" @"b"])
(Maybe.Just v) (= &(ser &v) "99")
_ false))
_ false)
"set-in replaces nested value")

(assert-true test
(let [j (JSON.obj [])
updated (JSON.set-in j &[@"a" @"b"] (JSON.Num 1.0))]
(match (JSON.get-in &updated &[@"a" @"b"])
(Maybe.Just v) (= &(ser &v) "1")
_ false))
"set-in creates intermediate objects for missing keys")

(assert-equal test
"[1,99,3]"
&(ser
&(JSON.set-in
(JSON.Arr
[(Box.init (JSON.Num 1.0))
(Box.init (JSON.Num 2.0))
(Box.init (JSON.Num 3.0))])
&[@"1"]
(JSON.Num 99.0)))
"set-in replaces array element by index")

(assert-true test
(match (JSON.parse "{\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]}")
(Result.Success j)
(let [updated (JSON.set-in j
&[@"users" @"1" @"name"]
(JSON.Str @"Charlie"))]
(match (JSON.get-in &updated &[@"users" @"1" @"name"])
(Maybe.Just v) (= &(ser &v) "\"Charlie\"")
_ false))
_ false)
"set-in modifies nested value in mixed object/array structure")

(assert-equal test
"[1,2]"
&(ser
&(JSON.set-in
(JSON.Arr [(Box.init (JSON.Num 1.0)) (Box.init (JSON.Num 2.0))])
&[@"5"]
(JSON.Num 99.0)))
"set-in on out-of-bounds array index returns unchanged")

(assert-equal test
"42"
&(ser &(JSON.set-in (JSON.Num 42.0) &[@"x"] (JSON.Num 1.0)))
"set-in on non-container returns unchanged")

(assert-true test
(let [j (JSON.obj [(JSON.entry @"x" (JSON.Num 10.0))])
updated (JSON.update-in j
&[@"x"]
(fn [n]
(match (JSON.as-num &n)
(Maybe.Just x) (JSON.Num (+ x 1.0))
(Maybe.Nothing) n)))]
(match (JSON.get &updated "x") (Maybe.Just v) (= &(ser &v) "11") _ false))
"update-in applies function to value at path")

(assert-true test
(match (JSON.parse "{\"a\":{\"b\":5}}")
(Result.Success j)
(let [updated (JSON.update-in j
&[@"a" @"b"]
(fn [n]
(match (JSON.as-num &n)
(Maybe.Just x) (JSON.Num (* x 2.0))
(Maybe.Nothing) n)))]
(match (JSON.get-in &updated &[@"a" @"b"])
(Maybe.Just v) (= &(ser &v) "10")
_ false))
_ false)
"update-in applies function at nested path")

(assert-true test
(let [j (JSON.obj [(JSON.entry @"x" (JSON.Num 1.0))])
updated (JSON.update-in j &[@"missing"] (fn [n] (JSON.Num 99.0)))]
(match (JSON.get &updated "x") (Maybe.Just v) (= &(ser &v) "1") _ false))
"update-in on missing path returns unchanged")

(assert-true test
(let [j (JSON.Arr [(Box.init (JSON.Num 10.0)) (Box.init (JSON.Num 20.0))])
updated (JSON.update-in j
&[@"0"]
(fn [n]
(match (JSON.as-num &n)
(Maybe.Just x) (JSON.Num (+ x 5.0))
(Maybe.Nothing) n)))]
(match (JSON.get-in &updated &[@"0"])
(Maybe.Just v) (= &(ser &v) "15")
_ false))
"update-in works with array indices"))