Initial commit
This commit is contained in:
commit
f07be54064
|
@ -0,0 +1,23 @@
|
||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: erlef/setup-beam@v1
|
||||||
|
with:
|
||||||
|
otp-version: "26.0.2"
|
||||||
|
gleam-version: "1.0.0"
|
||||||
|
rebar3-version: "3"
|
||||||
|
# elixir-version: "1.15.4"
|
||||||
|
- run: gleam deps download
|
||||||
|
- run: gleam test
|
||||||
|
- run: gleam format --check src test
|
|
@ -0,0 +1,4 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
|
@ -0,0 +1,28 @@
|
||||||
|
# jasper
|
||||||
|
|
||||||
|
[![Package Version](https://img.shields.io/hexpm/v/jasper)](https://hex.pm/packages/jasper)
|
||||||
|
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/jasper/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add jasper
|
||||||
|
```
|
||||||
|
```gleam
|
||||||
|
import gleam/io
|
||||||
|
import jasper.{parse_json, query_json, String, Root, Key, Index}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let assert Ok(json) = parse_json("{ \"foo\": [1, true, \"hi\"] }")
|
||||||
|
let assert Ok(String(str)) = query_json(json, Root |> Key("foo") |> Index(2))
|
||||||
|
io.println(str)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further documentation can be found at <https://hexdocs.pm/jasper>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam run # Run the project
|
||||||
|
gleam test # Run the tests
|
||||||
|
gleam shell # Run an Erlang shell
|
||||||
|
```
|
|
@ -0,0 +1,13 @@
|
||||||
|
name = "jasper"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
description = "Utilities for parsing and querying JSON data"
|
||||||
|
licences = ["AGPL-3.0-only"]
|
||||||
|
repository = { type = "github", user = "LilyRose2798", repo = "jasper" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = "~> 0.34 or ~> 1.0"
|
||||||
|
pears = "~> 0.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = "~> 1.0"
|
|
@ -0,0 +1,13 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
||||||
|
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
||||||
|
{ name = "pears", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "pears", source = "hex", outer_checksum = "F823EDF5C7F8606A0B7764C071B6EE8515FC341D6FC69902E81120633DF7E983" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
|
||||||
|
gleeunit = { version = "~> 1.0" }
|
||||||
|
pears = { version = "~> 0.3"}
|
|
@ -0,0 +1,197 @@
|
||||||
|
import gleam/float
|
||||||
|
import gleam/int
|
||||||
|
import gleam/string
|
||||||
|
import gleam/result
|
||||||
|
import gleam/option.{None, Some}
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import gleam/list
|
||||||
|
import pears.{type Parser}
|
||||||
|
import pears/chars.{type Char, digit, string}
|
||||||
|
import pears/combinators.{
|
||||||
|
alt, between, choice, eof, just, lazy, left, many0, many1, map, maybe, none_of,
|
||||||
|
one_of, pair, recognize, right, sep_by0, seq, to,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type JsonObject =
|
||||||
|
Dict(String, JsonValue)
|
||||||
|
|
||||||
|
pub type JsonArray =
|
||||||
|
List(JsonValue)
|
||||||
|
|
||||||
|
pub type JsonValue {
|
||||||
|
Object(JsonObject)
|
||||||
|
Array(JsonArray)
|
||||||
|
String(String)
|
||||||
|
Number(Float)
|
||||||
|
Boolean(Bool)
|
||||||
|
Null
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace0() -> Parser(Char, List(Char)) {
|
||||||
|
one_of([" ", "\n", "\r", "\t"])
|
||||||
|
|> many0()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_parser() -> Parser(Char, JsonValue) {
|
||||||
|
let padded = fn(parser: Parser(_, a)) { left(parser, whitespace0()) }
|
||||||
|
let symbol = fn(s: String) { padded(string(s)) }
|
||||||
|
|
||||||
|
let hex_digit =
|
||||||
|
one_of([
|
||||||
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e",
|
||||||
|
"f", "A", "B", "C", "D", "E", "F",
|
||||||
|
])
|
||||||
|
|
||||||
|
let unicode_escape_digits =
|
||||||
|
recognize(seq([hex_digit, hex_digit, hex_digit, hex_digit]))
|
||||||
|
|
||||||
|
let escape =
|
||||||
|
just("\\")
|
||||||
|
|> right(
|
||||||
|
choice([
|
||||||
|
just("\\"),
|
||||||
|
just("/"),
|
||||||
|
just("\""),
|
||||||
|
to(just("b"), "\u{0008}"),
|
||||||
|
to(just("f"), "\u{000C}"),
|
||||||
|
to(just("n"), "\n"),
|
||||||
|
to(just("r"), "\r"),
|
||||||
|
to(just("t"), "\t"),
|
||||||
|
map(right(just("u"), unicode_escape_digits), fn(value) {
|
||||||
|
let assert Ok(number) = int.base_parse(string.concat(value), 16)
|
||||||
|
let assert Ok(codepoint) = string.utf_codepoint(number)
|
||||||
|
string.from_utf_codepoints([codepoint])
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
let str =
|
||||||
|
none_of(["\""])
|
||||||
|
|> alt(escape)
|
||||||
|
|> many0()
|
||||||
|
|> map(string.concat)
|
||||||
|
|> between(just("\""), just("\""))
|
||||||
|
|
||||||
|
let value = lazy(value_parser)
|
||||||
|
|
||||||
|
let num =
|
||||||
|
maybe(just("-"))
|
||||||
|
|> pair(
|
||||||
|
alt(
|
||||||
|
to(just("0"), ["0"]),
|
||||||
|
recognize(pair(
|
||||||
|
one_of(["1", "2", "3", "4", "5", "6", "7", "8", "9"]),
|
||||||
|
many0(digit()),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|> map(string.concat),
|
||||||
|
)
|
||||||
|
|> pair(maybe(
|
||||||
|
just(".")
|
||||||
|
|> right(many1(digit()))
|
||||||
|
|> map(string.concat),
|
||||||
|
))
|
||||||
|
|> pair(
|
||||||
|
recognize(maybe(
|
||||||
|
alt(just("e"), just("E"))
|
||||||
|
|> pair(maybe(one_of(["+", "-"])))
|
||||||
|
|> pair(many1(digit())),
|
||||||
|
))
|
||||||
|
|> map(string.concat),
|
||||||
|
)
|
||||||
|
|> map(fn(p) {
|
||||||
|
case p {
|
||||||
|
#(#(#(neg, ns), ds), ex) -> {
|
||||||
|
{
|
||||||
|
option.unwrap(neg, "") <> ns <> "." <> option.unwrap(ds, "0") <> ex
|
||||||
|
}
|
||||||
|
|> float.parse
|
||||||
|
|> result.unwrap(case neg {
|
||||||
|
Some(_) -> -1.7976931348623158e308
|
||||||
|
None -> 1.7976931348623158e308
|
||||||
|
})
|
||||||
|
|> Number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let bool =
|
||||||
|
alt(to(string("true"), Boolean(True)), to(string("false"), Boolean(False)))
|
||||||
|
|
||||||
|
let null = to(string("null"), Null)
|
||||||
|
|
||||||
|
let array =
|
||||||
|
sep_by0(value, symbol(","))
|
||||||
|
|> between(symbol("["), symbol("]"))
|
||||||
|
|> map(Array)
|
||||||
|
|
||||||
|
let obj =
|
||||||
|
str
|
||||||
|
|> left(symbol(":"))
|
||||||
|
|> pair(value)
|
||||||
|
|> sep_by0(symbol(","))
|
||||||
|
|> map(dict.from_list)
|
||||||
|
|> between(symbol("{"), symbol("}"))
|
||||||
|
|> map(Object)
|
||||||
|
|
||||||
|
choice([num, bool, null, map(str, String), array, obj])
|
||||||
|
|> padded()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_parser() -> Parser(Char, JsonValue) {
|
||||||
|
value_parser()
|
||||||
|
|> between(whitespace0(), eof())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type JsonParseError {
|
||||||
|
UnexpectedToken(found: Char)
|
||||||
|
UnexpectedEndOfInput
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_json(value: String) -> Result(JsonValue, JsonParseError) {
|
||||||
|
case json_parser()(chars.input(value)) {
|
||||||
|
Ok(pears.Parsed(_, j)) -> Ok(j)
|
||||||
|
Error(e) ->
|
||||||
|
Error(case e {
|
||||||
|
pears.UnexpectedToken(_, _, f) -> UnexpectedToken(f)
|
||||||
|
pears.UnexpectedEndOfInput(_, _) -> UnexpectedEndOfInput
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type JsonQuery {
|
||||||
|
Root
|
||||||
|
Key(JsonQuery, key: String)
|
||||||
|
Index(JsonQuery, index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type JsonQueryError {
|
||||||
|
UnexpectedType(JsonValue)
|
||||||
|
MissingObjectKey(JsonValue, key: String)
|
||||||
|
IndexOutOfBounds(JsonValue, index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_json(
|
||||||
|
json: JsonValue,
|
||||||
|
query: JsonQuery,
|
||||||
|
) -> Result(JsonValue, JsonQueryError) {
|
||||||
|
case query {
|
||||||
|
Root -> Ok(json)
|
||||||
|
Key(q, k) ->
|
||||||
|
case query_json(json, q) {
|
||||||
|
Ok(Object(o) as j) ->
|
||||||
|
dict.get(o, k)
|
||||||
|
|> result.replace_error(MissingObjectKey(j, k))
|
||||||
|
Ok(j) -> Error(UnexpectedType(j))
|
||||||
|
x -> x
|
||||||
|
}
|
||||||
|
Index(q, i) ->
|
||||||
|
case query_json(json, q) {
|
||||||
|
Ok(Array(a) as j) ->
|
||||||
|
list.at(a, i)
|
||||||
|
|> result.replace_error(IndexOutOfBounds(j, i))
|
||||||
|
Ok(j) -> Error(UnexpectedType(j))
|
||||||
|
x -> x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
import gleeunit
|
||||||
|
import gleeunit/should
|
||||||
|
import gleam/dict
|
||||||
|
import jasper.{
|
||||||
|
type JsonValue, Array, Boolean, Index, IndexOutOfBounds, Key, MissingObjectKey,
|
||||||
|
Null, Number, Object, Root, String, UnexpectedType, parse_json, query_json,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_parse(json: String, result: JsonValue) {
|
||||||
|
json
|
||||||
|
|> parse_json
|
||||||
|
|> should.equal(Ok(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_numbers_test() {
|
||||||
|
should_parse("4.2", Number(4.2))
|
||||||
|
should_parse("42", Number(42.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_booleans_test() {
|
||||||
|
should_parse("true", Boolean(True))
|
||||||
|
should_parse("false", Boolean(False))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_null_test() {
|
||||||
|
should_parse("null", Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_strings_test() {
|
||||||
|
should_parse("\"hello\"", String("hello"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_arrays_test() {
|
||||||
|
should_parse("[]", Array([]))
|
||||||
|
should_parse("[1, 2, 3]", Array([Number(1.0), Number(2.0), Number(3.0)]))
|
||||||
|
should_parse(
|
||||||
|
"[true, false, null]",
|
||||||
|
Array([Boolean(True), Boolean(False), Null]),
|
||||||
|
)
|
||||||
|
should_parse(
|
||||||
|
"[\"hello\", \"world\"]",
|
||||||
|
Array([String("hello"), String("world")]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_objects_test() {
|
||||||
|
should_parse("{}", Object(dict.new()))
|
||||||
|
should_parse(
|
||||||
|
"{\"a\": 1, \"b\": 2}",
|
||||||
|
Object(dict.from_list([#("a", Number(1.0)), #("b", Number(2.0))])),
|
||||||
|
)
|
||||||
|
should_parse(
|
||||||
|
"{\"a\": true, \"b\": false, \"c\": null}",
|
||||||
|
Object(
|
||||||
|
dict.from_list([
|
||||||
|
#("a", Boolean(True)),
|
||||||
|
#("b", Boolean(False)),
|
||||||
|
#("c", Null),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
should_parse(
|
||||||
|
"{\"a\": \"hello\", \"b\": \"world\"}",
|
||||||
|
Object(dict.from_list([#("a", String("hello")), #("b", String("world"))])),
|
||||||
|
)
|
||||||
|
should_parse(
|
||||||
|
"{\"👋\": [1, 2, 3], \"b\": {\"c\": 4}}",
|
||||||
|
Object(
|
||||||
|
dict.from_list([
|
||||||
|
#("👋", Array([Number(1.0), Number(2.0), Number(3.0)])),
|
||||||
|
#("b", Object(dict.from_list([#("c", Number(4.0))]))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_test() {
|
||||||
|
query_json(String("foo"), Root)
|
||||||
|
|> should.equal(Ok(String("foo")))
|
||||||
|
query_json(
|
||||||
|
String("foo"),
|
||||||
|
Root
|
||||||
|
|> Key("foo"),
|
||||||
|
)
|
||||||
|
|> should.equal(Error(UnexpectedType(String("foo"))))
|
||||||
|
query_json(
|
||||||
|
String("foo"),
|
||||||
|
Root
|
||||||
|
|> Index(2),
|
||||||
|
)
|
||||||
|
|> should.equal(Error(UnexpectedType(String("foo"))))
|
||||||
|
query_json(
|
||||||
|
Array([String("foo")]),
|
||||||
|
Root
|
||||||
|
|> Index(2),
|
||||||
|
)
|
||||||
|
|> should.equal(Error(IndexOutOfBounds(Array([String("foo")]), 2)))
|
||||||
|
query_json(
|
||||||
|
Object(dict.from_list([#("bar", Array([String("foo")]))])),
|
||||||
|
Root
|
||||||
|
|> Key("bar")
|
||||||
|
|> Index(2),
|
||||||
|
)
|
||||||
|
|> should.equal(Error(IndexOutOfBounds(Array([String("foo")]), 2)))
|
||||||
|
query_json(
|
||||||
|
Object(dict.from_list([#("bar", Array([String("foo")]))])),
|
||||||
|
Root
|
||||||
|
|> Key("foo")
|
||||||
|
|> Index(2),
|
||||||
|
)
|
||||||
|
|> should.equal(
|
||||||
|
Error(MissingObjectKey(
|
||||||
|
Object(dict.from_list([#("bar", Array([String("foo")]))])),
|
||||||
|
"foo",
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue