@kaiko.io/rescript-deser 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +4 -0
- package/bsconfig.json +2 -5
- package/package.json +3 -1
- package/src/JSON.res +81 -16
- package/tests/QUnit.res +72 -0
- package/tests/index.res +192 -0
- package/www/index.html +20 -0
- package/tests/compile.res +0 -22
package/Makefile
CHANGED
|
@@ -18,3 +18,7 @@ RESCRIPT_FILES := $(shell find src -type f -name '*.res')
|
|
|
18
18
|
|
|
19
19
|
compile-rescript: $(RESCRIPT_FILES) yarn.lock
|
|
20
20
|
[ -n "$(INSIDE_EMACS)" ] && (rescript build -with-deps | sed "s@ $(shell pwd)/@@") || rescript build -with-deps
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
test: compile
|
|
24
|
+
esbuild lib/js/tests/index.js --bundle --outdir=www --servedir=www --serve=4369
|
package/bsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaiko.io/rescript-deser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"json",
|
|
6
6
|
"deserializer",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"@kaiko.io/rescript-prelude": "^3.0.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"esbuild": "^0.15.7",
|
|
19
|
+
"qunit": "^2.16.0",
|
|
18
20
|
"rescript": "^10.0.1"
|
|
19
21
|
}
|
|
20
22
|
}
|
package/src/JSON.res
CHANGED
|
@@ -27,6 +27,7 @@ module type Deserializer = {
|
|
|
27
27
|
type t
|
|
28
28
|
let name: string
|
|
29
29
|
let fromJSON: Js.Json.t => result<t, string>
|
|
30
|
+
let checkFieldsSanity: unit => result<unit, string>
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
module Field = {
|
|
@@ -68,6 +69,8 @@ module Field = {
|
|
|
68
69
|
// a Constraints for this in the future.
|
|
69
70
|
| Morphism(t, FieldValue.t => FieldValue.t)
|
|
70
71
|
|
|
72
|
+
| Self
|
|
73
|
+
|
|
71
74
|
let usingString = (f: string => 'a, value) => value->FieldValue.asString->f->FieldValue.any
|
|
72
75
|
let usingInt = (f: int => 'a, value) => value->FieldValue.asInt->f->FieldValue.any
|
|
73
76
|
let usingFloat = (f: float => 'a, value) => value->FieldValue.asFloat->f->FieldValue.any
|
|
@@ -104,6 +107,7 @@ module Field = {
|
|
|
104
107
|
| Boolean => "Boolean"
|
|
105
108
|
| Datetime
|
|
106
109
|
| Date => "Date"
|
|
110
|
+
| Self => "Self (recursive)"
|
|
107
111
|
| Collection(m) => {
|
|
108
112
|
module M = unpack(m: Deserializer)
|
|
109
113
|
"Collection of " ++ M.name
|
|
@@ -141,19 +145,24 @@ module Field = {
|
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
let rec extractValue = (
|
|
148
|
+
let rec extractValue = (
|
|
149
|
+
values: Dict.t<Js.Json.t>,
|
|
150
|
+
field: string,
|
|
151
|
+
shape: t,
|
|
152
|
+
self: t,
|
|
153
|
+
): FieldValue.t => {
|
|
145
154
|
switch values->Dict.get(field) {
|
|
146
|
-
| Some(value) => value->fromUntagged(shape)
|
|
155
|
+
| Some(value) => value->fromUntagged(shape, self)
|
|
147
156
|
| None =>
|
|
148
157
|
switch shape {
|
|
149
|
-
| DefaultWhenInvalid(_, _) => Js.Json.null->fromUntagged(shape)
|
|
150
|
-
| Optional(_) => Js.Json.null->fromUntagged(shape)
|
|
158
|
+
| DefaultWhenInvalid(_, _) => Js.Json.null->fromUntagged(shape, self)
|
|
159
|
+
| Optional(_) => Js.Json.null->fromUntagged(shape, self)
|
|
151
160
|
| OptionalWithDefault(_, default) => default
|
|
152
161
|
| _ => raise(TypeError(`Missing non-optional field '${field}'`))
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
and fromUntagged = (untagged: Js.Json.t, shape: t): FieldValue.t => {
|
|
165
|
+
and fromUntagged = (untagged: Js.Json.t, shape: t, self: t): FieldValue.t => {
|
|
157
166
|
switch (shape, untagged->Js.Json.classify) {
|
|
158
167
|
| (Any, _) => untagged->FieldValue.any
|
|
159
168
|
| (String, Js.Json.JSONString(text)) => FieldValue.string(text)
|
|
@@ -165,7 +174,7 @@ module Field = {
|
|
|
165
174
|
let lenbases = bases->Array.length
|
|
166
175
|
let lenitems = items->Array.length
|
|
167
176
|
if lenbases == lenitems {
|
|
168
|
-
let values = Array.zipBy(items, bases, fromUntagged)
|
|
177
|
+
let values = Array.zipBy(items, bases, (i, b) => fromUntagged(i, b, self))
|
|
169
178
|
values->FieldValue.array
|
|
170
179
|
} else {
|
|
171
180
|
raise(
|
|
@@ -193,14 +202,14 @@ module Field = {
|
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
| (Array(shape), Js.Json.JSONArray(items)) =>
|
|
196
|
-
FieldValue.array(items->Array.map(item => item->fromUntagged(shape)))
|
|
205
|
+
FieldValue.array(items->Array.map(item => item->fromUntagged(shape, self)))
|
|
197
206
|
| (Mapping(f), Js.Json.JSONObject(values)) =>
|
|
198
|
-
values->Dict.mapValues(v => v->fromUntagged(f))->FieldValue.mapping
|
|
207
|
+
values->Dict.mapValues(v => v->fromUntagged(f, self))->FieldValue.mapping
|
|
199
208
|
| (Object(fields), Js.Json.JSONObject(values)) =>
|
|
200
209
|
FieldValue.object(
|
|
201
210
|
fields
|
|
202
211
|
->Array.map(((field, shape)) => {
|
|
203
|
-
let value = switch extractValue(values, field, shape) {
|
|
212
|
+
let value = switch extractValue(values, field, shape, self) {
|
|
204
213
|
| value => value
|
|
205
214
|
| exception TypeError(msg) => raise(TypeError(`Field "${field}": ${msg}`))
|
|
206
215
|
}
|
|
@@ -210,10 +219,10 @@ module Field = {
|
|
|
210
219
|
)
|
|
211
220
|
|
|
212
221
|
| (OptionalWithDefault(_, value), Js.Json.JSONNull) => value
|
|
213
|
-
| (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape)
|
|
222
|
+
| (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape, self)
|
|
214
223
|
| (Optional(_), Js.Json.JSONNull) => FieldValue.null
|
|
215
|
-
| (Optional(shape), _) => untagged->fromUntagged(shape)
|
|
216
|
-
| (Morphism(shape, f), _) => untagged->fromUntagged(shape)->f->FieldValue.any
|
|
224
|
+
| (Optional(shape), _) => untagged->fromUntagged(shape, self)
|
|
225
|
+
| (Morphism(shape, f), _) => untagged->fromUntagged(shape, self)->f->FieldValue.any
|
|
217
226
|
|
|
218
227
|
| (Collection(m), Js.Json.JSONArray(items)) => {
|
|
219
228
|
module M = unpack(m: Deserializer)
|
|
@@ -234,18 +243,63 @@ module Field = {
|
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
| (DefaultWhenInvalid(t, default), _) =>
|
|
237
|
-
switch untagged->fromUntagged(t) {
|
|
246
|
+
switch untagged->fromUntagged(t, self) {
|
|
238
247
|
| res => res
|
|
239
248
|
| exception TypeError(msg) => {
|
|
240
249
|
Js.Console.warn2("Detected and ignore (with default): ", msg)
|
|
241
250
|
default
|
|
242
251
|
}
|
|
243
252
|
}
|
|
244
|
-
|
|
253
|
+
| (Self, _) => untagged->fromUntagged(self, self)
|
|
245
254
|
| (expected, actual) =>
|
|
246
255
|
raise(TypeError(`Expected ${expected->toString}, but got ${actual->_taggedToString} instead`))
|
|
247
256
|
}
|
|
248
257
|
}
|
|
258
|
+
|
|
259
|
+
let rec checkFieldsSanity = (name: string, fields: t, optional: bool): result<unit, string> =>
|
|
260
|
+
switch (fields, optional) {
|
|
261
|
+
| (Self, false) => Error(`${name}: Trivial infinite recursion 'let fields = Self'`)
|
|
262
|
+
| (Self, true) => Ok()
|
|
263
|
+
|
|
264
|
+
| (Any, _) => Ok()
|
|
265
|
+
| (String, _) | (Float, _) | (Int, _) => Ok()
|
|
266
|
+
| (Boolean, _) | (Date, _) | (Datetime, _) => Ok()
|
|
267
|
+
| (Morphism(_, _), _) => Ok()
|
|
268
|
+
|
|
269
|
+
| (Collection(mod), _)
|
|
270
|
+
| (Deserializer(mod), _) => {
|
|
271
|
+
module M = unpack(mod: Deserializer)
|
|
272
|
+
switch M.checkFieldsSanity() {
|
|
273
|
+
| Ok() => Ok()
|
|
274
|
+
| Error(msg) => Error(`${name}/ ${msg}`)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
| (DefaultWhenInvalid(fields, _), _)
|
|
279
|
+
| (OptionalWithDefault(fields, _), _)
|
|
280
|
+
| (Optional(fields), _) =>
|
|
281
|
+
checkFieldsSanity(name, fields, true)
|
|
282
|
+
|
|
283
|
+
| (Object(fields), optional) =>
|
|
284
|
+
fields
|
|
285
|
+
->Array.map(((fieldName, field), ()) =>
|
|
286
|
+
checkFieldsSanity(`${name}::${fieldName}`, field, optional)
|
|
287
|
+
)
|
|
288
|
+
->ManyResults.bailU
|
|
289
|
+
->ManyResults.map(_ => ())
|
|
290
|
+
|
|
291
|
+
/// Mappings and arrays can be empty, so their payloads are
|
|
292
|
+
/// automatically optional.
|
|
293
|
+
| (Mapping(field), _) | (Array(field), _) => checkFieldsSanity(name, field, true)
|
|
294
|
+
|
|
295
|
+
| (Tuple(fields), optional) =>
|
|
296
|
+
fields
|
|
297
|
+
->Array.mapWithIndex((index, field, ()) =>
|
|
298
|
+
checkFieldsSanity(`${name}[${index->Int.toString}]`, field, optional)
|
|
299
|
+
)
|
|
300
|
+
->ManyResults.bailU
|
|
301
|
+
->ManyResults.map(_ => ())
|
|
302
|
+
}
|
|
249
303
|
}
|
|
250
304
|
|
|
251
305
|
module type Serializable = {
|
|
@@ -261,9 +315,20 @@ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) =>
|
|
|
261
315
|
|
|
262
316
|
%%private(external _toNativeType: FieldValue.t => t = "%identity")
|
|
263
317
|
|
|
264
|
-
|
|
318
|
+
@doc("Checks for trivial infinite-recursion in the fields of the module.
|
|
319
|
+
|
|
320
|
+
Notice this algorithm is just an heuristic, and it might happen that are
|
|
321
|
+
cases of infinite-recursion not detected and cases where detection is a
|
|
322
|
+
false positive.
|
|
323
|
+
|
|
324
|
+
You should use this only while debugging/developing to verify your data.
|
|
325
|
+
|
|
326
|
+
")
|
|
327
|
+
let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
|
|
328
|
+
|
|
329
|
+
@doc("Parse a `Js.Json.t` into `result<t, string>`")
|
|
265
330
|
let fromJSON = (json: Js.Json.t): result<t, _> => {
|
|
266
|
-
switch json->Field.fromUntagged(fields) {
|
|
331
|
+
switch json->Field.fromUntagged(fields, fields) {
|
|
267
332
|
| res => Ok(res->_toNativeType)
|
|
268
333
|
| exception TypeError(e) => Error(e)
|
|
269
334
|
}
|
package/tests/QUnit.res
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
type description = string
|
|
2
|
+
type message = string
|
|
3
|
+
|
|
4
|
+
type result<'a> = {
|
|
5
|
+
result: bool,
|
|
6
|
+
actual: 'a,
|
|
7
|
+
expected: 'a,
|
|
8
|
+
message: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type assertion
|
|
12
|
+
type block = assertion => unit
|
|
13
|
+
type matcher<'a> = 'a => bool
|
|
14
|
+
|
|
15
|
+
// Library stuff
|
|
16
|
+
|
|
17
|
+
@scope("QUnit") external start: unit => unit = "start"
|
|
18
|
+
@scope("QUnit") external done: (unit => unit) => unit = "done"
|
|
19
|
+
|
|
20
|
+
@send external expect: (assertion, int) => unit = "expect"
|
|
21
|
+
@send external pushResult: (assertion, result<'a>) => unit = "notOk"
|
|
22
|
+
@send external step: (assertion, description) => unit = "step"
|
|
23
|
+
@send external verifySteps: (assertion, array<description>) => unit = "verifySteps"
|
|
24
|
+
|
|
25
|
+
// Values
|
|
26
|
+
|
|
27
|
+
@send external equal: (assertion, 'a, 'a, description) => unit = "equal"
|
|
28
|
+
@send external notEqual: (assertion, 'a, 'a, description) => unit = "notEqual"
|
|
29
|
+
|
|
30
|
+
@send external isFalse: (assertion, 'a, description) => unit = "false"
|
|
31
|
+
@send external isTrue: (assertion, 'a, description) => unit = "true"
|
|
32
|
+
|
|
33
|
+
@send external deepEqual: (assertion, 'a, 'a, description) => unit = "deepEqual"
|
|
34
|
+
@send external notDeepEqual: (assertion, 'a, 'a, description) => unit = "notDeepEqual"
|
|
35
|
+
|
|
36
|
+
@send external strictEqual: (assertion, 'a, 'a, description) => unit = "strictEqual"
|
|
37
|
+
@send external notStrictEqual: (assertion, 'a, 'a, description) => unit = "notStrictEqual"
|
|
38
|
+
|
|
39
|
+
@send external ok: (assertion, 'a, 'a) => unit = "ok"
|
|
40
|
+
@send external notOk: (assertion, 'a, 'a) => unit = "notOk"
|
|
41
|
+
|
|
42
|
+
@send external propEqual: (assertion, 'a, 'a) => unit = "propEqual"
|
|
43
|
+
@send external notPropEqual: (assertion, 'a, 'a) => unit = "notPropEqual"
|
|
44
|
+
|
|
45
|
+
// Promises
|
|
46
|
+
|
|
47
|
+
type done = unit => unit
|
|
48
|
+
|
|
49
|
+
@send external async: assertion => done = "async"
|
|
50
|
+
@send external asyncMany: (assertion, int) => done = "async"
|
|
51
|
+
@send external rejects: (assertion, Js.Promise.t<'a>, message) => unit = "rejects"
|
|
52
|
+
@send external rejectsM: (assertion, Js.Promise.t<'a>, message) => unit = "rejects"
|
|
53
|
+
@send
|
|
54
|
+
external rejectMatches: (assertion, Js.Promise.t<'a>, matcher<'a>, message) => unit = "rejects"
|
|
55
|
+
@send
|
|
56
|
+
external rejectMatchesM: (assertion, Js.Promise.t<'a>, matcher<'a>, message) => unit = "rejects"
|
|
57
|
+
@send external timeout: (assertion, int) => unit = "timeout"
|
|
58
|
+
|
|
59
|
+
// Exceptions
|
|
60
|
+
|
|
61
|
+
@send external throws: (assertion, block, message) => unit = "throws"
|
|
62
|
+
@send
|
|
63
|
+
external throwMatches: (assertion, block, matcher<'a>, message) => unit = "throws"
|
|
64
|
+
|
|
65
|
+
type hooks
|
|
66
|
+
@send external before: (hooks, block) => unit = "before"
|
|
67
|
+
@send external beforeEach: (hooks, block) => unit = "beforeEach"
|
|
68
|
+
@send external afterEach: (hooks, block) => unit = "afterEach"
|
|
69
|
+
@send external after: (hooks, block) => unit = "after"
|
|
70
|
+
|
|
71
|
+
@module("qunit") @val external module_: (description, hooks => unit) => unit = "module"
|
|
72
|
+
@module("qunit") @val external test: (description, block) => unit = "test"
|
package/tests/index.res
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
open JSON
|
|
2
|
+
open QUnit
|
|
3
|
+
open Prelude
|
|
4
|
+
|
|
5
|
+
%%private(external cast: 'a => 'b = "%identity")
|
|
6
|
+
|
|
7
|
+
module Appointment = {
|
|
8
|
+
type t = {
|
|
9
|
+
note: string,
|
|
10
|
+
date: Js.Date.t,
|
|
11
|
+
extra: option<string>,
|
|
12
|
+
}
|
|
13
|
+
module Deserializer = MakeDeserializer({
|
|
14
|
+
type t = t
|
|
15
|
+
open JSON.Field
|
|
16
|
+
|
|
17
|
+
let fields = Object([("note", String), ("date", Date), ("extra", Optional(String))])
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module_("Basic deserializer", _ => {
|
|
22
|
+
let valid: array<(_, Appointment.t)> = [
|
|
23
|
+
(
|
|
24
|
+
%raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
|
|
25
|
+
{
|
|
26
|
+
note: "Bicentennial doctor's appointment",
|
|
27
|
+
date: Js.Date.fromString("2100-01-01"),
|
|
28
|
+
extra: None,
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
%raw(`{
|
|
33
|
+
"note": "Bicentennial doctor's appointment",
|
|
34
|
+
"date": "2100-01-01",
|
|
35
|
+
"extra": "Don't take your stop-aging pills the night before the appointment",
|
|
36
|
+
}`),
|
|
37
|
+
{
|
|
38
|
+
note: "Bicentennial doctor's appointment",
|
|
39
|
+
date: Js.Date.fromString("2100-01-01"),
|
|
40
|
+
extra: Some("Don't take your stop-aging pills the night before the appointment"),
|
|
41
|
+
},
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
test("Correctly deserializes data", qunit => {
|
|
46
|
+
qunit->expect(valid->Array.length)
|
|
47
|
+
valid->Array.forEach(
|
|
48
|
+
((data, expected)) => {
|
|
49
|
+
Js.Console.log2("Running sample", data)
|
|
50
|
+
switch Appointment.Deserializer.fromJSON(data) {
|
|
51
|
+
| Ok(result) => qunit->deepEqual(result, expected, "result == expected")
|
|
52
|
+
| Error(msg) => Js.Console.error(msg)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
let invalid = [
|
|
59
|
+
(
|
|
60
|
+
"Missing non-optional field",
|
|
61
|
+
%raw(`{"extra": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
test("Correctly catches invalid data", qunit => {
|
|
66
|
+
qunit->expect(invalid->Array.length)
|
|
67
|
+
invalid->Array.forEach(
|
|
68
|
+
((message, data)) => {
|
|
69
|
+
Js.Console.log3("Running sample", message, data)
|
|
70
|
+
switch Appointment.Deserializer.fromJSON(data) {
|
|
71
|
+
| Ok(result) => Js.Console.error2("Invalid being accepted: ", result)
|
|
72
|
+
| Error(msg) => {
|
|
73
|
+
Js.Console.log2("Correctly detected:", msg)
|
|
74
|
+
qunit->ok(true, true)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
module_("Recursive deserializer", _ => {
|
|
83
|
+
module TrivialTree = {
|
|
84
|
+
type rec t<'a> = {
|
|
85
|
+
data: 'a,
|
|
86
|
+
children: array<t<'a>>,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module DeserializerImpl = MakeDeserializer({
|
|
90
|
+
type payload
|
|
91
|
+
type rec t = {
|
|
92
|
+
data: payload,
|
|
93
|
+
children: array<t>,
|
|
94
|
+
}
|
|
95
|
+
open JSON.Field
|
|
96
|
+
|
|
97
|
+
let fields = Object([("data", Any), ("children", Array(Self))])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
module Deserializer = {
|
|
101
|
+
include DeserializerImpl
|
|
102
|
+
|
|
103
|
+
let fromJSON = data => data->DeserializerImpl.fromJSON->Result.map(cast)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let valid: array<(_, TrivialTree.t<string>)> = [
|
|
108
|
+
(
|
|
109
|
+
%raw(`{"data": "A", "children": [{"data": "A1", "children": []}, {"data": "B", "children": [{"data": "C", "children": []}, {"data": "D", "children": []}]}]}`),
|
|
110
|
+
{
|
|
111
|
+
data: "A",
|
|
112
|
+
children: [
|
|
113
|
+
{data: "A1", children: []},
|
|
114
|
+
{data: "B", children: [{data: "C", children: []}, {data: "D", children: []}]},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
test("Trivial recursion detection: Ok", qunit => {
|
|
121
|
+
qunit->expect(1)
|
|
122
|
+
qunit->deepEqual(TrivialTree.Deserializer.checkFieldsSanity(), Ok(), "Ok")
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("Infinite list", qunit => {
|
|
126
|
+
module InfiniteList = MakeDeserializer({
|
|
127
|
+
open JSON.Field
|
|
128
|
+
|
|
129
|
+
type t
|
|
130
|
+
let fields = Object([("head", String), ("tail", Self)])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
qunit->expect(1)
|
|
134
|
+
qunit->deepEqual(InfiniteList.checkFieldsSanity()->Result.isError, true, "Ok")
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("Finite list", qunit => {
|
|
138
|
+
module List = MakeDeserializer({
|
|
139
|
+
open JSON.Field
|
|
140
|
+
|
|
141
|
+
type t
|
|
142
|
+
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
qunit->expect(1)
|
|
146
|
+
qunit->deepEqual(List.checkFieldsSanity(), Ok(), "Ok")
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test("Correctly deserializes recursive data", qunit => {
|
|
150
|
+
qunit->expect(valid->Array.length)
|
|
151
|
+
valid->Array.forEach(
|
|
152
|
+
((data, expected)) => {
|
|
153
|
+
Js.Console.log2("Running sample", data)
|
|
154
|
+
switch TrivialTree.Deserializer.fromJSON(data) {
|
|
155
|
+
| Ok(result) => qunit->deepEqual(result, expected, "result == expected")
|
|
156
|
+
| Error(msg) => Js.Console.error(msg)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("Recursion in sub-deserializer", qunit => {
|
|
163
|
+
module List = MakeDeserializer({
|
|
164
|
+
open JSON.Field
|
|
165
|
+
|
|
166
|
+
type t
|
|
167
|
+
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
module Ledger = MakeDeserializer({
|
|
171
|
+
open JSON.Field
|
|
172
|
+
|
|
173
|
+
type t
|
|
174
|
+
let fields = Object([("records", Deserializer(module(List))), ("next", Optional(Self))])
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
let data = %raw(`
|
|
178
|
+
{"records": {"head": "A", "tail": {"head": "B"}},
|
|
179
|
+
"next": {"records": {"head": "A", "tail": {"head": "B"}}}}
|
|
180
|
+
`)
|
|
181
|
+
let expected = {
|
|
182
|
+
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
183
|
+
"next": {
|
|
184
|
+
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
185
|
+
"next": None,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
qunit->expect(1)
|
|
190
|
+
qunit->deepEqual(data->Ledger.fromJSON->cast, Ok(expected), "nice ledger")
|
|
191
|
+
})
|
|
192
|
+
})
|
package/www/index.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<link rel="shortcut icon" href="#" />
|
|
9
|
+
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.16.0.css" type="text/css" media="screen" />
|
|
10
|
+
<title>Document</title>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<script type="module" defer src="index.js"></script>
|
|
14
|
+
|
|
15
|
+
<body>
|
|
16
|
+
<div id="qunit"></div>
|
|
17
|
+
<div id="qunit-fixture"></div>
|
|
18
|
+
</body>
|
|
19
|
+
|
|
20
|
+
</html>
|
package/tests/compile.res
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
open JSON
|
|
2
|
-
|
|
3
|
-
module Test = {
|
|
4
|
-
type appointment = {
|
|
5
|
-
note: string,
|
|
6
|
-
date: Js.Date.t,
|
|
7
|
-
}
|
|
8
|
-
module Appointment = MakeDeserializer({
|
|
9
|
-
type t = appointment
|
|
10
|
-
open JSON.Field
|
|
11
|
-
|
|
12
|
-
let fields = Object([("note", String), ("date", Date)])
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
type foo = array<appointment>
|
|
16
|
-
module Foo = MakeDeserializer({
|
|
17
|
-
type t = foo
|
|
18
|
-
open JSON.Field
|
|
19
|
-
|
|
20
|
-
let fields = DefaultWhenInvalid(Array(Deserializer(module(Appointment))), []->FieldValue.array)
|
|
21
|
-
})
|
|
22
|
-
}
|