Skip to content

Commit

Permalink
Add JsonExtractor utility class (#19)
Browse files Browse the repository at this point in the history
The extractor wraps standard boilerplate for working with JsonType
structures into a utility class that makes for easier reading
code when pulling values of known types from a parsed Json structure.
  • Loading branch information
SeanTAllen authored Jan 22, 2025
1 parent f6a18ef commit 7ba00a5
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .release-notes/19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Add JsonExtractor utility class

We've added a new class to the `JSON` library. `JsonExtractor` bundles up a lot of the boilerplate needed to extract typed values from Json objects.

Given the following Json:

```json
{
"name": "John",
"age": 30,
"isStudent": true
}
```

Where you previously had to do:

```pony
let doc = recover val JsonDoc.>parse(src)? end
let name = (doc.data as JsonOject).data("name")? as String
let age = (doc.data as JsonOject).data("age")? as I64
let isStudent = (doc.data as JsonOject).data("isStudent")? as Bool
```

You can now do:

```pony
let doc = recover val JsonDoc.>parse(src)? end
let name = JsonExtractor(doc.data)("name")?.as_string()?
let age = JsonExtractor(doc.data)("age")?.as_i64()?
let isStudent = JsonExtractor(doc.data)("isStudent")?.as_bool()?
```

For simple Json structures such as the one above, there is little difference. However, once you start dealing with nested objects and arrays, `JsonExtractor` can save you a lot of boilerplate code.
61 changes: 61 additions & 0 deletions json/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ actor \nodoc\ Main is TestList

fun tag tests(test: PonyTest) =>
// Tests below function across all systems and are listed alphabetically
test(_TestExtractor)
test(_TestNoPrettyPrintArray)
test(_TestNoPrettyPrintObject)
test(_TestParseArray)
Expand Down Expand Up @@ -642,3 +643,63 @@ class \nodoc\ iso _TestParsePrint is UnitTest
expect.remove("\r")

h.assert_eq[String ref](expect, actual)

class \nodoc\ iso _TestExtractor is UnitTest
"""
Test JsonExtractor
"""
fun name(): String => "JSON/extractor"

fun apply(h: TestHelper) ? =>
let src =
"""
[
{
"precision": "zip",
"population": 10,
"objection": {
"fu": "bar"
}
},
{
"data": [
"Really?",
"yes",
true,
4,
12.3
]
},
47,
{
"foo": {
"bar": [
{
"aardvark": null
},
false
]
}
}
]"""

let doc = recover val JsonDoc.>parse(src)? end
let array = JsonExtractor(doc.data).as_array()?
h.assert_eq[USize](4, array.size())
// Array index 0
h.assert_eq[String]("zip", JsonExtractor(array(0)?)("precision")?.as_string()?)
h.assert_eq[I64](10, JsonExtractor(array(0)?)("population")?.as_i64()?)
h.assert_eq[String]("bar", JsonExtractor(array(0)?)("objection")?("fu")?.as_string()?)
// Array index 1
h.assert_eq[USize](5, JsonExtractor(array(1)?)("data")?.size()?)
h.assert_eq[String]("Really?", JsonExtractor(array(1)?)("data")?(0)?.as_string()?)
h.assert_eq[String]("yes", JsonExtractor(array(1)?)("data")?(1)?.as_string()?)
h.assert_eq[Bool](true, JsonExtractor(array(1)?)("data")?(2)?.as_bool()?)
h.assert_eq[I64](4, JsonExtractor(array(1)?)("data")?(3)?.as_i64()?)
h.assert_eq[F64](12.3, JsonExtractor(array(1)?)("data")?(4)?.as_f64()?)
// Array index 2
h.assert_eq[I64](47, JsonExtractor(array(2)?).as_i64()?)
// Array index 3
h.assert_eq[USize](2, JsonExtractor(array(3)?)("foo")?("bar")?.size()?)
h.assert_eq[None](None, JsonExtractor(array(3)?)("foo")?("bar")?(0)?("aardvark")?.as_none()?)
h.assert_eq[Bool](false, JsonExtractor(array(3)?)("foo")?("bar")?(1)?.as_bool()?)
191 changes: 191 additions & 0 deletions json/json_extractor.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use "collections"

class val JsonExtractor
"""
Utility class for working with JSON structures.
Given the following JSON:
```json
{
"environments": [
{
"name": "corral-env",
"user": "sean",
"image": "an-image:latest",
"shell": "fish",
"workdir": "/workspace",
"workspace": "/home/sean/ponylang/corral",
"mounts": [
{"source":"/var/run/docker.sock","target":"/var/run/docker.sock", "type": "bind"}
]
}
]
}
```
We can use the following code to extract our values:
```pony
primitive Parser
fun apply(json: JsonType val): Array[Environment] val ? =>
recover val
let envs = JsonExtractor(json)("environments")?.as_array()?
let result: Array[Environment] = Array[Environment]
for e in envs.values() do
let obj = JsonExtractor(e).as_object()?
let name = JsonExtractor(obj("name")?).as_string()?
let user = JsonExtractor(obj("user")?).as_string()?
let image = JsonExtractor(obj("image")?).as_string()?
let shell = JsonExtractor(obj("shell")?).as_string()?
let workdir = JsonExtractor(obj("workdir")?).as_string()?
let workspace = JsonExtractor(obj("workspace")?).as_string()?
let mounts = recover trn Array[Mount] end
for i in JsonExtractor(obj("mounts")?).as_array()?.values() do
let m = MountParser(i)?
mounts.push(m)
end
let environment = Environment(name, user, image, shell, workdir, workspace, consume mounts)
result.push(environment)
end
result
end
primitive MountParser
fun apply(json: JsonType val): Mount ? =>
let obj = JsonExtractor(json).as_object()?
let source = JsonExtractor(obj("source")?).as_string()?
let target = JsonExtractor(obj("target")?).as_string()?
let mtype = JsonExtractor(obj("type")?).as_string()?
Mount(source, target, mtype)
```
The JsonExtractor creates a lot of intermediate objects, but it makes the code
easier to read and understand. We suggest not using it in critical paths where
performance is a concern.
"""
let _json: JsonType val

new val create(json: JsonType val) =>
"""
Create a new JsonExtractor from a JSON structure.
"""
_json = json

fun val apply(idx_or_key: (String | USize)): JsonExtractor val ? =>
"""
Extract an array or object by index or key and return a new JsonExtractor.
"""
match (_json, idx_or_key)
| (let a: JsonArray val, let idx: USize) =>
JsonExtractor((a.data)(idx)?)
| (let o: JsonObject val, let key: String) =>
JsonExtractor((o.data)(key)?)
else
error
end

fun val size(): USize ? =>
"""
Return the size of the JSON structure.
Results in an error for any structure that isn't a `JsonArray` or `JsonObject`.
"""
match _json
| let a: JsonArray val =>
a.data.size()
| let o: JsonObject val =>
o.data.size()
else
error
end

fun val values(): Iterator[JsonType val] ? =>
"""
Return an iterator over the values of the JSON structure.
Results in an error for any structure that isn't a `JsonArray`.
"""
match _json
| let a: JsonArray val =>
a.data.values()
else
error
end

fun val pairs(): Iterator[(String, JsonType val)] ? =>
"""
Return a pairs iterator over the values of the JSON structure.
Results in an error for any structure that isn't a `JsonArray`.
"""
match _json
| let o: JsonObject val =>
o.data.pairs()
else
error
end

fun val as_array(): Array[JsonType] val ? =>
"""
Extract an Array from the JSON structure.
"""
match _json
| let a: JsonArray val =>
a.data
else
error
end

fun val as_object(): Map[String, JsonType] val ? =>
"""
Extract a Map from the JSON structure.
"""
match _json
| let o: JsonObject val =>
o.data
else
error
end

fun val as_string(): String ? =>
"""
Extract a String from the JSON structure.
"""
_json as String

fun val as_none(): None ? =>
"""
Extract a None from the JSON structure.
"""
_json as None

fun val as_f64(): F64 ? =>
"""
Extract a F64 from the JSON structure.
"""
_json as F64

fun val as_i64(): I64 ? =>
"""
Extract a I64 from the JSON structure.
"""
_json as I64

fun val as_bool(): Bool ? =>
"""
Extract a Bool from the JSON structure.
"""
_json as Bool

fun val as_string_or_none(): (String | None) ? =>
"""
Extract a String or None from the JSON structure.
"""
match _json
| let s: String => s
| let n: None => n
else
error
end

0 comments on commit 7ba00a5

Please sign in to comment.