@kaiko.io/rescript-deser 7.0.0 → 8.0.0-alpha.1
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/CHANGELOG.md +163 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/Deser.ast +0 -0
- package/lib/bs/src/Deser.cmi +0 -0
- package/lib/bs/src/Deser.cmj +0 -0
- package/lib/bs/src/Deser.cmt +0 -0
- package/lib/bs/src/Deser.res +88 -3
- package/lib/bs/tests/index.ast +0 -0
- package/lib/bs/tests/index.cmi +0 -0
- package/lib/bs/tests/index.cmj +0 -0
- package/lib/bs/tests/index.cmt +0 -0
- package/lib/bs/tests/index.res +176 -31
- package/lib/es6/src/Deser.js +117 -3
- package/lib/es6/tests/index.js +221 -62
- package/lib/js/src/Deser.js +117 -3
- package/lib/js/tests/index.js +221 -62
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/Deser.ast +0 -0
- package/lib/ocaml/Deser.cmi +0 -0
- package/lib/ocaml/Deser.cmj +0 -0
- package/lib/ocaml/Deser.cmt +0 -0
- package/lib/ocaml/Deser.res +88 -3
- package/lib/ocaml/index.ast +0 -0
- package/lib/ocaml/index.cmi +0 -0
- package/lib/ocaml/index.cmj +0 -0
- package/lib/ocaml/index.cmt +0 -0
- package/lib/ocaml/index.res +176 -31
- package/lib/rescript.lock +1 -1
- package/package.json +1 -1
- package/src/Deser.res +88 -3
- package/tests/index.res +176 -31
package/lib/ocaml/index.res
CHANGED
|
@@ -4,7 +4,7 @@ module Appointment = {
|
|
|
4
4
|
type t = {
|
|
5
5
|
note: string,
|
|
6
6
|
date: Date.t,
|
|
7
|
-
extra
|
|
7
|
+
extra?: string,
|
|
8
8
|
}
|
|
9
9
|
module Deserializer = Deser.MakeDeserializer({
|
|
10
10
|
type t = t
|
|
@@ -14,15 +14,42 @@ module Appointment = {
|
|
|
14
14
|
})
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
module List = {
|
|
18
|
+
type rec t = {
|
|
19
|
+
head: string,
|
|
20
|
+
tail?: t,
|
|
21
|
+
}
|
|
22
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
23
|
+
type t = t
|
|
24
|
+
open Deser.Field
|
|
25
|
+
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module Ledger = {
|
|
30
|
+
type rec t = {
|
|
31
|
+
records: List.t,
|
|
32
|
+
next?: t,
|
|
33
|
+
}
|
|
34
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
35
|
+
type t = t
|
|
36
|
+
open Deser.Field
|
|
37
|
+
let fields = Object([
|
|
38
|
+
("records", Deserializer(module(List.Deserializer))),
|
|
39
|
+
("next", Optional(Self)),
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
module_("Basic deserializer", _ => {
|
|
18
45
|
let valid: array<(_, Appointment.t)> = [
|
|
19
46
|
(
|
|
20
47
|
%raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
|
|
21
48
|
{
|
|
22
|
-
note: "Bicentennial doctor's appointment",
|
|
23
|
-
date: Date.fromString("2100-01-01"),
|
|
24
|
-
extra: None,
|
|
25
|
-
},
|
|
49
|
+
"note": "Bicentennial doctor's appointment",
|
|
50
|
+
"date": Date.fromString("2100-01-01"),
|
|
51
|
+
"extra": None, // Deser always return `undefined` for missing, optional fields
|
|
52
|
+
}->Obj.magic,
|
|
26
53
|
),
|
|
27
54
|
(
|
|
28
55
|
%raw(`{
|
|
@@ -33,7 +60,7 @@ module_("Basic deserializer", _ => {
|
|
|
33
60
|
{
|
|
34
61
|
note: "Bicentennial doctor's appointment",
|
|
35
62
|
date: Date.fromString("2100-01-01"),
|
|
36
|
-
extra:
|
|
63
|
+
extra: "Don't take your stop-aging pills the night before the appointment",
|
|
37
64
|
},
|
|
38
65
|
),
|
|
39
66
|
]
|
|
@@ -131,15 +158,8 @@ module_("Recursive deserializer", _ => {
|
|
|
131
158
|
})
|
|
132
159
|
|
|
133
160
|
test("Finite list", qunit => {
|
|
134
|
-
module List = Deser.MakeDeserializer({
|
|
135
|
-
open Deser.Field
|
|
136
|
-
|
|
137
|
-
type t
|
|
138
|
-
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
139
|
-
})
|
|
140
|
-
|
|
141
161
|
qunit->expect(1)
|
|
142
|
-
qunit->deepEqual(List.checkFieldsSanity(), Ok(), "Ok")
|
|
162
|
+
qunit->deepEqual(List.Deserializer.checkFieldsSanity(), Ok(), "Ok")
|
|
143
163
|
})
|
|
144
164
|
|
|
145
165
|
test("Correctly deserializes recursive data", qunit => {
|
|
@@ -156,34 +176,21 @@ module_("Recursive deserializer", _ => {
|
|
|
156
176
|
})
|
|
157
177
|
|
|
158
178
|
test("Recursion in sub-deserializer", qunit => {
|
|
159
|
-
module List = Deser.MakeDeserializer({
|
|
160
|
-
open Deser.Field
|
|
161
|
-
|
|
162
|
-
type t
|
|
163
|
-
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
module Ledger = Deser.MakeDeserializer({
|
|
167
|
-
open Deser.Field
|
|
168
|
-
|
|
169
|
-
type t
|
|
170
|
-
let fields = Object([("records", Deserializer(module(List))), ("next", Optional(Self))])
|
|
171
|
-
})
|
|
172
|
-
|
|
173
179
|
let data = %raw(`
|
|
174
180
|
{"records": {"head": "A", "tail": {"head": "B"}},
|
|
175
181
|
"next": {"records": {"head": "A", "tail": {"head": "B"}}}}
|
|
176
182
|
`)
|
|
177
|
-
|
|
183
|
+
// Deser always returns `undefined` for missing optional fields
|
|
184
|
+
let expected: Ledger.t = {
|
|
178
185
|
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
179
186
|
"next": {
|
|
180
187
|
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
181
188
|
"next": None,
|
|
182
189
|
},
|
|
183
|
-
}
|
|
190
|
+
}->Obj.magic
|
|
184
191
|
|
|
185
192
|
qunit->expect(1)
|
|
186
|
-
qunit->deepEqual(data->Ledger.fromJSON
|
|
193
|
+
qunit->deepEqual(data->Ledger.Deserializer.fromJSON, Ok(expected), "nice ledger")
|
|
187
194
|
})
|
|
188
195
|
})
|
|
189
196
|
|
|
@@ -210,3 +217,141 @@ module_("Type safety limits", _ => {
|
|
|
210
217
|
)
|
|
211
218
|
})
|
|
212
219
|
})
|
|
220
|
+
|
|
221
|
+
module_("isSafeCast and toJSON", _ => {
|
|
222
|
+
module SafePrimitives = {
|
|
223
|
+
type t = {
|
|
224
|
+
name: string,
|
|
225
|
+
count: int,
|
|
226
|
+
ratio: float,
|
|
227
|
+
active: bool,
|
|
228
|
+
}
|
|
229
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
230
|
+
type t = t
|
|
231
|
+
open Deser.Field
|
|
232
|
+
let fields = Object([("name", String), ("count", Int), ("ratio", Float), ("active", Boolean)])
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module SafeNested = {
|
|
237
|
+
type t = {
|
|
238
|
+
items: array<string>,
|
|
239
|
+
tags: dict<int>,
|
|
240
|
+
label?: string,
|
|
241
|
+
}
|
|
242
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
243
|
+
type t = t
|
|
244
|
+
open Deser.Field
|
|
245
|
+
let fields = Object([
|
|
246
|
+
("items", Array(String)),
|
|
247
|
+
("tags", Mapping(Int)),
|
|
248
|
+
("label", Optional(String)),
|
|
249
|
+
])
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module UnsafeWithDate = {
|
|
254
|
+
type t = {
|
|
255
|
+
name: string,
|
|
256
|
+
created: Date.t,
|
|
257
|
+
}
|
|
258
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
259
|
+
type t = t
|
|
260
|
+
open Deser.Field
|
|
261
|
+
let fields = Object([("name", String), ("created", Date)])
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module NestedUnsafe = {
|
|
266
|
+
type t = {event: UnsafeWithDate.t}
|
|
267
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
268
|
+
type t = t
|
|
269
|
+
open Deser.Field
|
|
270
|
+
let fields = Object([("event", Deserializer(module(UnsafeWithDate.Deserializer)))])
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
test("Safe types: primitives return true for isSafeCast", qunit => {
|
|
275
|
+
qunit->expect(1)
|
|
276
|
+
qunit->isTrue(SafePrimitives.Deserializer.isSafeCast(), "primitives are safe")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("Safe types: nested arrays/mappings/optionals return true for isSafeCast", qunit => {
|
|
280
|
+
qunit->expect(1)
|
|
281
|
+
qunit->isTrue(SafeNested.Deserializer.isSafeCast(), "nested safe types are safe")
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test("Unsafe types: Date returns false for isSafeCast", qunit => {
|
|
285
|
+
qunit->expect(1)
|
|
286
|
+
qunit->isFalse(UnsafeWithDate.Deserializer.isSafeCast(), "Date is unsafe")
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("Unsafe types: nested deserializer with Date returns false", qunit => {
|
|
290
|
+
qunit->expect(1)
|
|
291
|
+
qunit->isFalse(NestedUnsafe.Deserializer.isSafeCast(), "nested unsafe is unsafe")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test("Round-trip for safe types", qunit => {
|
|
295
|
+
let json: JSON.t = %raw(`{"name": "test", "count": 42, "ratio": 3.14, "active": true}`)
|
|
296
|
+
switch SafePrimitives.Deserializer.fromJSON(json) {
|
|
297
|
+
| Ok(value) => {
|
|
298
|
+
let serialized = SafePrimitives.Deserializer.toJSON(value)
|
|
299
|
+
qunit->deepEqual(serialized, json, "round-trip preserves data")
|
|
300
|
+
}
|
|
301
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test("Round-trip for unsafe types with Date", qunit => {
|
|
306
|
+
let json: JSON.t = %raw(`{"name": "event", "created": "2024-01-15T10:30:00.000Z"}`)
|
|
307
|
+
switch UnsafeWithDate.Deserializer.fromJSON(json) {
|
|
308
|
+
| Ok(value) => {
|
|
309
|
+
let serialized = UnsafeWithDate.Deserializer.toJSON(value)
|
|
310
|
+
qunit->deepEqual(serialized, json, "round-trip preserves data with Date")
|
|
311
|
+
}
|
|
312
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("Nested deserializer safety propagation", qunit => {
|
|
317
|
+
let json: JSON.t = %raw(`{"event": {"name": "meeting", "created": "2024-06-01T09:00:00.000Z"}}`)
|
|
318
|
+
switch NestedUnsafe.Deserializer.fromJSON(json) {
|
|
319
|
+
| Ok(value) => {
|
|
320
|
+
let serialized = NestedUnsafe.Deserializer.toJSON(value)
|
|
321
|
+
qunit->deepEqual(serialized, json, "nested unsafe round-trip works")
|
|
322
|
+
}
|
|
323
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test("Morphism of safe type is safe", qunit => {
|
|
328
|
+
module WithMorphism = Deser.MakeDeserializer({
|
|
329
|
+
type t = int
|
|
330
|
+
open Deser.Field
|
|
331
|
+
let fields = Morphism(Int, v => v)
|
|
332
|
+
})
|
|
333
|
+
qunit->expect(1)
|
|
334
|
+
qunit->isTrue(WithMorphism.isSafeCast(), "morphism of Int is safe")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test("List deserializer is safe (recursive with Self)", qunit => {
|
|
338
|
+
qunit->expect(1)
|
|
339
|
+
qunit->isTrue(List.Deserializer.isSafeCast(), "recursive list with strings is safe")
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("Appointment deserializer is unsafe (has Date)", qunit => {
|
|
343
|
+
qunit->expect(1)
|
|
344
|
+
qunit->isFalse(Appointment.Deserializer.isSafeCast(), "Appointment with Date is unsafe")
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("toJSON serializes Date to ISO string", qunit => {
|
|
348
|
+
let appointment: Appointment.t = {
|
|
349
|
+
note: "Test",
|
|
350
|
+
date: Date.fromString("2024-03-15T14:30:00.000Z"),
|
|
351
|
+
extra: "extra info",
|
|
352
|
+
}
|
|
353
|
+
let json = Appointment.Deserializer.toJSON(appointment)
|
|
354
|
+
let expected: JSON.t = %raw(`{"note": "Test", "date": "2024-03-15T14:30:00.000Z", "extra": "extra info"}`)
|
|
355
|
+
qunit->deepEqual(json, expected, "Date serialized to ISO string")
|
|
356
|
+
})
|
|
357
|
+
})
|
package/lib/rescript.lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
130543
|
package/package.json
CHANGED
package/src/Deser.res
CHANGED
|
@@ -25,7 +25,9 @@ module type Deserializer = {
|
|
|
25
25
|
type t
|
|
26
26
|
let name: string
|
|
27
27
|
let fromJSON: JSON.t => result<t, string>
|
|
28
|
+
let toJSON: t => JSON.t
|
|
28
29
|
let checkFieldsSanity: unit => result<unit, string>
|
|
30
|
+
let isSafeCast: unit => bool
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
module Field = {
|
|
@@ -305,6 +307,66 @@ module Field = {
|
|
|
305
307
|
)
|
|
306
308
|
->Result.map(_ => ())
|
|
307
309
|
}
|
|
310
|
+
|
|
311
|
+
let rec isSafeCast = (field: t): bool =>
|
|
312
|
+
switch field {
|
|
313
|
+
// Any is considered *safe* because it SHOULD only be used if the type you're getting from
|
|
314
|
+
// JSON.t is compatible with the internal representation anyways.
|
|
315
|
+
| Any => true
|
|
316
|
+
| String | Literal(_) | Int | Float | Boolean | Self => true
|
|
317
|
+
| Date | Datetime => false
|
|
318
|
+
| Morphism(inner, _) => isSafeCast(inner)
|
|
319
|
+
| Array(inner)
|
|
320
|
+
| Optional(inner)
|
|
321
|
+
| OptionalWithDefault(inner, _)
|
|
322
|
+
| Mapping(inner)
|
|
323
|
+
| DefaultWhenInvalid(inner, _) =>
|
|
324
|
+
isSafeCast(inner)
|
|
325
|
+
| Tuple(fields) => fields->Array.every(isSafeCast)
|
|
326
|
+
| Object(fields) => fields->Array.every(((_, f)) => isSafeCast(f))
|
|
327
|
+
| Deserializer(mod) | Collection(mod) => {
|
|
328
|
+
module M = unpack(mod: Deserializer)
|
|
329
|
+
M.isSafeCast()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let rec serialize: ('a, t, t) => JSON.t = (value, shape, self) =>
|
|
334
|
+
switch shape {
|
|
335
|
+
| Any | String | Int | Float | Boolean => value->Obj.magic
|
|
336
|
+
| Literal(v) => JSON.String(v)
|
|
337
|
+
| Self => serialize(value, self, self)
|
|
338
|
+
| Date | Datetime => JSON.String(value->Obj.magic->Date.toISOString)
|
|
339
|
+
|
|
340
|
+
| Morphism(inner, _) => serialize(value, inner, self)
|
|
341
|
+
| Array(inner) => JSON.Array(value->Obj.magic->Array.map(v => serialize(v, inner, self)))
|
|
342
|
+
|
|
343
|
+
| Tuple(fields) =>
|
|
344
|
+
JSON.Array(Belt.Array.zipBy(value->Obj.magic, fields, (v, f) => serialize(v, f, self)))
|
|
345
|
+
|
|
346
|
+
| Object(fields) =>
|
|
347
|
+
JSON.Object(
|
|
348
|
+
fields
|
|
349
|
+
->Array.filterMap(((key, fieldType)) =>
|
|
350
|
+
value
|
|
351
|
+
->Obj.magic
|
|
352
|
+
->Dict.get(key)
|
|
353
|
+
->Option.map(fieldValue => (key, serialize(fieldValue, fieldType, self)))
|
|
354
|
+
)
|
|
355
|
+
->Dict.fromArray,
|
|
356
|
+
)
|
|
357
|
+
| Optional(inner) =>
|
|
358
|
+
switch value->Obj.magic {
|
|
359
|
+
| Some(v) => serialize(v, inner, self)
|
|
360
|
+
| None => JSON.Null
|
|
361
|
+
}
|
|
362
|
+
| OptionalWithDefault(inner, _) | DefaultWhenInvalid(inner, _) => serialize(value, inner, self)
|
|
363
|
+
| Mapping(inner) =>
|
|
364
|
+
JSON.Object(value->Obj.magic->Dict.mapValues(v => serialize(v, inner, self)))
|
|
365
|
+
| Deserializer(mod) | Collection(mod) => {
|
|
366
|
+
module M = unpack(mod: Deserializer)
|
|
367
|
+
M.toJSON(value->Obj.magic)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
308
370
|
}
|
|
309
371
|
|
|
310
372
|
module type Serializable = {
|
|
@@ -315,10 +377,24 @@ module type Serializable = {
|
|
|
315
377
|
module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) => {
|
|
316
378
|
type t = S.t
|
|
317
379
|
let fields = S.fields
|
|
318
|
-
|
|
380
|
+
let (loc, _f) = __LOC_OF__(module(S: Serializable))
|
|
319
381
|
let name = `Deserializer ${__MODULE__}, ${loc}`
|
|
320
382
|
|
|
321
|
-
|
|
383
|
+
external _toNativeType: FieldValue.t => t = "%identity"
|
|
384
|
+
external _fromNativeType: t => FieldValue.t = "%identity"
|
|
385
|
+
|
|
386
|
+
let safeCastCache: ref<option<bool>> = ref(None)
|
|
387
|
+
|
|
388
|
+
let isSafeCast = () => {
|
|
389
|
+
switch safeCastCache.contents {
|
|
390
|
+
| Some(cached) => cached
|
|
391
|
+
| None => {
|
|
392
|
+
let result = Field.isSafeCast(fields)
|
|
393
|
+
safeCastCache := Some(result)
|
|
394
|
+
result
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
322
398
|
|
|
323
399
|
@doc("Checks for trivial infinite-recursion in the fields of the module.
|
|
324
400
|
|
|
@@ -331,11 +407,20 @@ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) =>
|
|
|
331
407
|
")
|
|
332
408
|
let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
|
|
333
409
|
|
|
334
|
-
@doc("Parse a `
|
|
410
|
+
@doc("Parse a `JSON.t` into `result<t, string>`")
|
|
335
411
|
let fromJSON: JSON.t => result<t, _> = json => {
|
|
336
412
|
switch json->Field.fromUntagged(fields, fields) {
|
|
337
413
|
| res => Ok(res->_toNativeType)
|
|
338
414
|
| exception TypeError(e) => Error(e)
|
|
339
415
|
}
|
|
340
416
|
}
|
|
417
|
+
|
|
418
|
+
@doc("Serialize a `t` into `JSON.t`")
|
|
419
|
+
let toJSON: t => JSON.t = value => {
|
|
420
|
+
if isSafeCast() {
|
|
421
|
+
value->Obj.magic
|
|
422
|
+
} else {
|
|
423
|
+
Field.serialize(value, fields, fields)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
341
426
|
}
|
package/tests/index.res
CHANGED
|
@@ -4,7 +4,7 @@ module Appointment = {
|
|
|
4
4
|
type t = {
|
|
5
5
|
note: string,
|
|
6
6
|
date: Date.t,
|
|
7
|
-
extra
|
|
7
|
+
extra?: string,
|
|
8
8
|
}
|
|
9
9
|
module Deserializer = Deser.MakeDeserializer({
|
|
10
10
|
type t = t
|
|
@@ -14,15 +14,42 @@ module Appointment = {
|
|
|
14
14
|
})
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
module List = {
|
|
18
|
+
type rec t = {
|
|
19
|
+
head: string,
|
|
20
|
+
tail?: t,
|
|
21
|
+
}
|
|
22
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
23
|
+
type t = t
|
|
24
|
+
open Deser.Field
|
|
25
|
+
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module Ledger = {
|
|
30
|
+
type rec t = {
|
|
31
|
+
records: List.t,
|
|
32
|
+
next?: t,
|
|
33
|
+
}
|
|
34
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
35
|
+
type t = t
|
|
36
|
+
open Deser.Field
|
|
37
|
+
let fields = Object([
|
|
38
|
+
("records", Deserializer(module(List.Deserializer))),
|
|
39
|
+
("next", Optional(Self)),
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
module_("Basic deserializer", _ => {
|
|
18
45
|
let valid: array<(_, Appointment.t)> = [
|
|
19
46
|
(
|
|
20
47
|
%raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
|
|
21
48
|
{
|
|
22
|
-
note: "Bicentennial doctor's appointment",
|
|
23
|
-
date: Date.fromString("2100-01-01"),
|
|
24
|
-
extra: None,
|
|
25
|
-
},
|
|
49
|
+
"note": "Bicentennial doctor's appointment",
|
|
50
|
+
"date": Date.fromString("2100-01-01"),
|
|
51
|
+
"extra": None, // Deser always return `undefined` for missing, optional fields
|
|
52
|
+
}->Obj.magic,
|
|
26
53
|
),
|
|
27
54
|
(
|
|
28
55
|
%raw(`{
|
|
@@ -33,7 +60,7 @@ module_("Basic deserializer", _ => {
|
|
|
33
60
|
{
|
|
34
61
|
note: "Bicentennial doctor's appointment",
|
|
35
62
|
date: Date.fromString("2100-01-01"),
|
|
36
|
-
extra:
|
|
63
|
+
extra: "Don't take your stop-aging pills the night before the appointment",
|
|
37
64
|
},
|
|
38
65
|
),
|
|
39
66
|
]
|
|
@@ -131,15 +158,8 @@ module_("Recursive deserializer", _ => {
|
|
|
131
158
|
})
|
|
132
159
|
|
|
133
160
|
test("Finite list", qunit => {
|
|
134
|
-
module List = Deser.MakeDeserializer({
|
|
135
|
-
open Deser.Field
|
|
136
|
-
|
|
137
|
-
type t
|
|
138
|
-
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
139
|
-
})
|
|
140
|
-
|
|
141
161
|
qunit->expect(1)
|
|
142
|
-
qunit->deepEqual(List.checkFieldsSanity(), Ok(), "Ok")
|
|
162
|
+
qunit->deepEqual(List.Deserializer.checkFieldsSanity(), Ok(), "Ok")
|
|
143
163
|
})
|
|
144
164
|
|
|
145
165
|
test("Correctly deserializes recursive data", qunit => {
|
|
@@ -156,34 +176,21 @@ module_("Recursive deserializer", _ => {
|
|
|
156
176
|
})
|
|
157
177
|
|
|
158
178
|
test("Recursion in sub-deserializer", qunit => {
|
|
159
|
-
module List = Deser.MakeDeserializer({
|
|
160
|
-
open Deser.Field
|
|
161
|
-
|
|
162
|
-
type t
|
|
163
|
-
let fields = Object([("head", String), ("tail", Optional(Self))])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
module Ledger = Deser.MakeDeserializer({
|
|
167
|
-
open Deser.Field
|
|
168
|
-
|
|
169
|
-
type t
|
|
170
|
-
let fields = Object([("records", Deserializer(module(List))), ("next", Optional(Self))])
|
|
171
|
-
})
|
|
172
|
-
|
|
173
179
|
let data = %raw(`
|
|
174
180
|
{"records": {"head": "A", "tail": {"head": "B"}},
|
|
175
181
|
"next": {"records": {"head": "A", "tail": {"head": "B"}}}}
|
|
176
182
|
`)
|
|
177
|
-
|
|
183
|
+
// Deser always returns `undefined` for missing optional fields
|
|
184
|
+
let expected: Ledger.t = {
|
|
178
185
|
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
179
186
|
"next": {
|
|
180
187
|
"records": {"head": "A", "tail": {"head": "B", "tail": None}},
|
|
181
188
|
"next": None,
|
|
182
189
|
},
|
|
183
|
-
}
|
|
190
|
+
}->Obj.magic
|
|
184
191
|
|
|
185
192
|
qunit->expect(1)
|
|
186
|
-
qunit->deepEqual(data->Ledger.fromJSON
|
|
193
|
+
qunit->deepEqual(data->Ledger.Deserializer.fromJSON, Ok(expected), "nice ledger")
|
|
187
194
|
})
|
|
188
195
|
})
|
|
189
196
|
|
|
@@ -210,3 +217,141 @@ module_("Type safety limits", _ => {
|
|
|
210
217
|
)
|
|
211
218
|
})
|
|
212
219
|
})
|
|
220
|
+
|
|
221
|
+
module_("isSafeCast and toJSON", _ => {
|
|
222
|
+
module SafePrimitives = {
|
|
223
|
+
type t = {
|
|
224
|
+
name: string,
|
|
225
|
+
count: int,
|
|
226
|
+
ratio: float,
|
|
227
|
+
active: bool,
|
|
228
|
+
}
|
|
229
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
230
|
+
type t = t
|
|
231
|
+
open Deser.Field
|
|
232
|
+
let fields = Object([("name", String), ("count", Int), ("ratio", Float), ("active", Boolean)])
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module SafeNested = {
|
|
237
|
+
type t = {
|
|
238
|
+
items: array<string>,
|
|
239
|
+
tags: dict<int>,
|
|
240
|
+
label?: string,
|
|
241
|
+
}
|
|
242
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
243
|
+
type t = t
|
|
244
|
+
open Deser.Field
|
|
245
|
+
let fields = Object([
|
|
246
|
+
("items", Array(String)),
|
|
247
|
+
("tags", Mapping(Int)),
|
|
248
|
+
("label", Optional(String)),
|
|
249
|
+
])
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module UnsafeWithDate = {
|
|
254
|
+
type t = {
|
|
255
|
+
name: string,
|
|
256
|
+
created: Date.t,
|
|
257
|
+
}
|
|
258
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
259
|
+
type t = t
|
|
260
|
+
open Deser.Field
|
|
261
|
+
let fields = Object([("name", String), ("created", Date)])
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module NestedUnsafe = {
|
|
266
|
+
type t = {event: UnsafeWithDate.t}
|
|
267
|
+
module Deserializer = Deser.MakeDeserializer({
|
|
268
|
+
type t = t
|
|
269
|
+
open Deser.Field
|
|
270
|
+
let fields = Object([("event", Deserializer(module(UnsafeWithDate.Deserializer)))])
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
test("Safe types: primitives return true for isSafeCast", qunit => {
|
|
275
|
+
qunit->expect(1)
|
|
276
|
+
qunit->isTrue(SafePrimitives.Deserializer.isSafeCast(), "primitives are safe")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("Safe types: nested arrays/mappings/optionals return true for isSafeCast", qunit => {
|
|
280
|
+
qunit->expect(1)
|
|
281
|
+
qunit->isTrue(SafeNested.Deserializer.isSafeCast(), "nested safe types are safe")
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test("Unsafe types: Date returns false for isSafeCast", qunit => {
|
|
285
|
+
qunit->expect(1)
|
|
286
|
+
qunit->isFalse(UnsafeWithDate.Deserializer.isSafeCast(), "Date is unsafe")
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("Unsafe types: nested deserializer with Date returns false", qunit => {
|
|
290
|
+
qunit->expect(1)
|
|
291
|
+
qunit->isFalse(NestedUnsafe.Deserializer.isSafeCast(), "nested unsafe is unsafe")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test("Round-trip for safe types", qunit => {
|
|
295
|
+
let json: JSON.t = %raw(`{"name": "test", "count": 42, "ratio": 3.14, "active": true}`)
|
|
296
|
+
switch SafePrimitives.Deserializer.fromJSON(json) {
|
|
297
|
+
| Ok(value) => {
|
|
298
|
+
let serialized = SafePrimitives.Deserializer.toJSON(value)
|
|
299
|
+
qunit->deepEqual(serialized, json, "round-trip preserves data")
|
|
300
|
+
}
|
|
301
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test("Round-trip for unsafe types with Date", qunit => {
|
|
306
|
+
let json: JSON.t = %raw(`{"name": "event", "created": "2024-01-15T10:30:00.000Z"}`)
|
|
307
|
+
switch UnsafeWithDate.Deserializer.fromJSON(json) {
|
|
308
|
+
| Ok(value) => {
|
|
309
|
+
let serialized = UnsafeWithDate.Deserializer.toJSON(value)
|
|
310
|
+
qunit->deepEqual(serialized, json, "round-trip preserves data with Date")
|
|
311
|
+
}
|
|
312
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("Nested deserializer safety propagation", qunit => {
|
|
317
|
+
let json: JSON.t = %raw(`{"event": {"name": "meeting", "created": "2024-06-01T09:00:00.000Z"}}`)
|
|
318
|
+
switch NestedUnsafe.Deserializer.fromJSON(json) {
|
|
319
|
+
| Ok(value) => {
|
|
320
|
+
let serialized = NestedUnsafe.Deserializer.toJSON(value)
|
|
321
|
+
qunit->deepEqual(serialized, json, "nested unsafe round-trip works")
|
|
322
|
+
}
|
|
323
|
+
| Error(msg) => qunit->isFalse(true, msg)
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test("Morphism of safe type is safe", qunit => {
|
|
328
|
+
module WithMorphism = Deser.MakeDeserializer({
|
|
329
|
+
type t = int
|
|
330
|
+
open Deser.Field
|
|
331
|
+
let fields = Morphism(Int, v => v)
|
|
332
|
+
})
|
|
333
|
+
qunit->expect(1)
|
|
334
|
+
qunit->isTrue(WithMorphism.isSafeCast(), "morphism of Int is safe")
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test("List deserializer is safe (recursive with Self)", qunit => {
|
|
338
|
+
qunit->expect(1)
|
|
339
|
+
qunit->isTrue(List.Deserializer.isSafeCast(), "recursive list with strings is safe")
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("Appointment deserializer is unsafe (has Date)", qunit => {
|
|
343
|
+
qunit->expect(1)
|
|
344
|
+
qunit->isFalse(Appointment.Deserializer.isSafeCast(), "Appointment with Date is unsafe")
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("toJSON serializes Date to ISO string", qunit => {
|
|
348
|
+
let appointment: Appointment.t = {
|
|
349
|
+
note: "Test",
|
|
350
|
+
date: Date.fromString("2024-03-15T14:30:00.000Z"),
|
|
351
|
+
extra: "extra info",
|
|
352
|
+
}
|
|
353
|
+
let json = Appointment.Deserializer.toJSON(appointment)
|
|
354
|
+
let expected: JSON.t = %raw(`{"note": "Test", "date": "2024-03-15T14:30:00.000Z", "extra": "extra info"}`)
|
|
355
|
+
qunit->deepEqual(json, expected, "Date serialized to ISO string")
|
|
356
|
+
})
|
|
357
|
+
})
|