Skip to content

Commit 941bd23

Browse files
committed
initial
1 parent e43a159 commit 941bd23

File tree

7 files changed

+298
-0
lines changed

7 files changed

+298
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
.idea
3+
*.log
4+
tmp/
5+
6+
.bsb.lock
7+
.merlin
8+
lib/
9+
node_modules/

bsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@glennsl/rescript-json-combinators",
3+
"namespace": "JsonCombinators",
4+
"sources": [
5+
"src",
6+
{
7+
"dir": "examples",
8+
"type": "dev"
9+
}
10+
]
11+
}

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@glennsl/rescript-json-combinators",
3+
"version": "1.0.0",
4+
"description": "Combinator library for JSON decoding and encoding. ",
5+
"scripts": {
6+
"build": "rescript build -with-deps",
7+
"start": "rescript build -w -with-deps",
8+
"clean": "rescript clean",
9+
"fmt": "rescript format -all"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/glennsl/rescript-json-combinators.git"
14+
},
15+
"keywords": ["rescript", "json"],
16+
"author": "glennsl",
17+
"license": "(LGPL-3.0 OR MPL-2.0)",
18+
"bugs": {
19+
"url": "https://github.com/glennsl/rescript-json-combinators/issues"
20+
},
21+
"homepage": "https://github.com/glennsl/rescript-json-combinators#readme",
22+
"files": ["src/*.res*", "bsconfig.json"],
23+
"devDependencies": {
24+
"rescript": "^9.1.4"
25+
}
26+
}

src/Decode.res

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
open Js.Json
2+
3+
type t<'a> = (. Js.Json.t) => 'a
4+
5+
type fieldDecoders = {
6+
optional: 'a. (. string, t<'a>) => option<'a>,
7+
required: 'a. (. string, t<'a>) => 'a,
8+
}
9+
10+
exception DecodeError(string)
11+
12+
module Error = {
13+
let expected = (kind, json) => raise(DecodeError(`Expected ${kind}, got ${stringify(json)}`))
14+
}
15+
16+
let custom = f => f
17+
18+
let float = (. json) => {
19+
if Js.typeof(json) != "number" {
20+
Error.expected("float", json)
21+
}
22+
23+
Obj.magic(json)
24+
}
25+
26+
let int = (. json) => {
27+
if Js.typeof(json) != "number" {
28+
Error.expected("int", json)
29+
}
30+
31+
let truncated: float = %raw("json | 0")
32+
let num: float = Obj.magic(json)
33+
if truncated != num || !Js.Float.isFinite(num) {
34+
Error.expected("int", json)
35+
}
36+
37+
Obj.magic(json)
38+
}
39+
40+
let bool = (. json) => {
41+
if Js.typeof(json) != "boolean" {
42+
Error.expected("bool", json)
43+
}
44+
45+
Obj.magic(json)
46+
}
47+
48+
let string = (. json) => {
49+
if Js.typeof(json) != "string" {
50+
Error.expected("string", json)
51+
}
52+
53+
Obj.magic(json)
54+
}
55+
56+
let array = (decode, . json) => {
57+
if !Js.Array.isArray(json) {
58+
Error.expected("array", json)
59+
}
60+
61+
let source: array<Js.Json.t> = Obj.magic(json)
62+
let target = %raw("new Array(json.length)")
63+
64+
for i in 0 to Array.length(source) - 1 {
65+
try {
66+
let value = decode(. %raw("json[i]"))
67+
target->Array.unsafe_set(i, value)
68+
} catch {
69+
| DecodeError(msg) => raise(DecodeError(j`${msg}\n\tin array at index $i`))
70+
}
71+
}
72+
73+
target
74+
}
75+
76+
let pair = (decodeA, decodeB, . json) => {
77+
if !Js.Array.isArray(json) {
78+
Error.expected("array", json)
79+
}
80+
81+
let arr: array<Js.Json.t> = Obj.magic(json)
82+
if Array.length(arr) != 2 {
83+
raise(
84+
DecodeError(
85+
`Expected array of length 2, got array of length ${Array.length(arr)->string_of_int}`,
86+
),
87+
)
88+
}
89+
90+
try (decodeA(. arr->Array.unsafe_get(0)), decodeB(. arr->Array.unsafe_get(1))) catch {
91+
| DecodeError(msg) => raise(DecodeError(j`${msg}\n\tin pair`))
92+
}
93+
}
94+
95+
let option = (decode, . json) => {
96+
if Obj.magic(json) == Js.null {
97+
None
98+
} else {
99+
Some(decode(. json))
100+
}
101+
}
102+
103+
let field = (key, decode, . json) => {
104+
if Js.typeof(json) != "object" || Js.Array.isArray(json) || Obj.magic(json) == Js.null {
105+
Error.expected("object", json)
106+
}
107+
108+
if !(%raw("key in json")) {
109+
raise(DecodeError(`${key} required`))
110+
}
111+
112+
try decode(. %raw("json[key]")) catch {
113+
| DecodeError(msg) => raise(DecodeError(`${msg}\n\tat field '${key}'`))
114+
}
115+
}
116+
117+
let object = (f, . json) => {
118+
if Js.typeof(json) != "object" || Js.Array.isArray(json) || Obj.magic(json) == Js.null {
119+
raise(Error.expected("object", json))
120+
}
121+
122+
let optional = (. key, decode) => {
123+
if !(%raw("key in json")) {
124+
None
125+
} else {
126+
try {
127+
let value = decode(. %raw("json[key]"))
128+
Some(value)
129+
} catch {
130+
| DecodeError(msg) => raise(DecodeError(`${msg}\n\tat field '${key}'`))
131+
}
132+
}
133+
}
134+
135+
let required = (. key, decode) => {
136+
if !(%raw("key in json")) {
137+
raise(DecodeError(`${key} required`))
138+
}
139+
140+
try decode(. %raw("json[key]")) catch {
141+
| DecodeError(msg) => raise(DecodeError(`${msg}\n\tat field '${key}'`))
142+
}
143+
}
144+
145+
f({optional: optional, required: required})
146+
}
147+
148+
let oneOf = (decoders, . json) => {
149+
let errors = []
150+
151+
let rec loop = i => {
152+
if i >= Array.length(decoders) {
153+
raise(
154+
DecodeError(
155+
`All decoders given to oneOf failed. Here are all the errors:\n- ${errors->Js.Array2.joinWith(
156+
"\n",
157+
)}\nAnd the JSON being decoded: ${stringify(json)}`,
158+
),
159+
)
160+
}
161+
162+
let decode = Array.unsafe_get(decoders, i)
163+
try decode(. json) catch {
164+
| DecodeError(err) =>
165+
errors->Js.Array2.push(err)->ignore
166+
loop(i + 1)
167+
}
168+
}
169+
170+
loop(0)
171+
}
172+
173+
let map = (decode, f, . json) => f(. decode(. json))
174+
175+
let decode = (json, decode) =>
176+
try Ok(decode(. json)) catch {
177+
| DecodeError(msg) => Error(msg)
178+
}

src/Decode.resi

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type t<'a>
2+
3+
type fieldDecoders = {
4+
optional: 'a. (. string, t<'a>) => option<'a>,
5+
required: 'a. (. string, t<'a>) => 'a,
6+
}
7+
8+
exception DecodeError(string)
9+
10+
module Error: {
11+
let expected: (string, Js.Json.t) => _
12+
}
13+
14+
let custom: ((. Js.Json.t) => 'a) => t<'a>
15+
16+
let float: t<float>
17+
let int: t<int>
18+
let bool: t<bool>
19+
let string: t<string>
20+
let array: t<'a> => t<array<'a>>
21+
let pair: (t<'a>, t<'b>) => t<('a, 'b)>
22+
let option: t<'a> => t<option<'a>>
23+
let field: (string, t<'a>) => t<'a>
24+
let object: (fieldDecoders => 'a) => t<'a>
25+
26+
let oneOf: array<t<'a>> => t<'a>
27+
let map: (t<'a>, (. 'a) => 'b) => t<'b>
28+
29+
let decode: (Js.Json.t, t<'a>) => result<'a, string>

src/Encode.res

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
external int: int => Js.Json.t = "%identity"
2+
external float: float => Js.Json.t = "%identity"
3+
external bool: bool => Js.Json.t = "%identity"
4+
external string: string => Js.Json.t = "%identity"
5+
6+
module Unsafe = {
7+
external object: {..} => Js.Json.t = "%identity"
8+
}

0 commit comments

Comments
 (0)