@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/js/tests/index.js
CHANGED
|
@@ -35,6 +35,59 @@ let Appointment = {
|
|
|
35
35
|
Deserializer: Deserializer
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
let fields$1 = {
|
|
39
|
+
TAG: "Object",
|
|
40
|
+
_0: [
|
|
41
|
+
[
|
|
42
|
+
"head",
|
|
43
|
+
"String"
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"tail",
|
|
47
|
+
{
|
|
48
|
+
TAG: "Optional",
|
|
49
|
+
_0: "Self"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let Deserializer$1 = Deser.MakeDeserializer({
|
|
56
|
+
fields: fields$1
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let List = {
|
|
60
|
+
Deserializer: Deserializer$1
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let fields$2 = {
|
|
64
|
+
TAG: "Object",
|
|
65
|
+
_0: [
|
|
66
|
+
[
|
|
67
|
+
"records",
|
|
68
|
+
{
|
|
69
|
+
TAG: "Deserializer",
|
|
70
|
+
_0: Deserializer$1
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"next",
|
|
75
|
+
{
|
|
76
|
+
TAG: "Optional",
|
|
77
|
+
_0: "Self"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let Deserializer$2 = Deser.MakeDeserializer({
|
|
84
|
+
fields: fields$2
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
let Ledger = {
|
|
88
|
+
Deserializer: Deserializer$2
|
|
89
|
+
};
|
|
90
|
+
|
|
38
91
|
Qunit.module("Basic deserializer", param => {
|
|
39
92
|
let valid = [
|
|
40
93
|
[
|
|
@@ -165,27 +218,8 @@ Qunit.module("Recursive deserializer", param => {
|
|
|
165
218
|
qunit.deepEqual(Stdlib_Result.isError(InfiniteList.checkFieldsSanity()), true, "Ok");
|
|
166
219
|
});
|
|
167
220
|
Qunit.test("Finite list", qunit => {
|
|
168
|
-
let fields = {
|
|
169
|
-
TAG: "Object",
|
|
170
|
-
_0: [
|
|
171
|
-
[
|
|
172
|
-
"head",
|
|
173
|
-
"String"
|
|
174
|
-
],
|
|
175
|
-
[
|
|
176
|
-
"tail",
|
|
177
|
-
{
|
|
178
|
-
TAG: "Optional",
|
|
179
|
-
_0: "Self"
|
|
180
|
-
}
|
|
181
|
-
]
|
|
182
|
-
]
|
|
183
|
-
};
|
|
184
|
-
let List = Deser.MakeDeserializer({
|
|
185
|
-
fields: fields
|
|
186
|
-
});
|
|
187
221
|
qunit.expect(1);
|
|
188
|
-
qunit.deepEqual(
|
|
222
|
+
qunit.deepEqual(Deserializer$1.checkFieldsSanity(), {
|
|
189
223
|
TAG: "Ok",
|
|
190
224
|
_0: undefined
|
|
191
225
|
}, "Ok");
|
|
@@ -204,47 +238,6 @@ Qunit.module("Recursive deserializer", param => {
|
|
|
204
238
|
});
|
|
205
239
|
});
|
|
206
240
|
Qunit.test("Recursion in sub-deserializer", qunit => {
|
|
207
|
-
let fields = {
|
|
208
|
-
TAG: "Object",
|
|
209
|
-
_0: [
|
|
210
|
-
[
|
|
211
|
-
"head",
|
|
212
|
-
"String"
|
|
213
|
-
],
|
|
214
|
-
[
|
|
215
|
-
"tail",
|
|
216
|
-
{
|
|
217
|
-
TAG: "Optional",
|
|
218
|
-
_0: "Self"
|
|
219
|
-
}
|
|
220
|
-
]
|
|
221
|
-
]
|
|
222
|
-
};
|
|
223
|
-
let List = Deser.MakeDeserializer({
|
|
224
|
-
fields: fields
|
|
225
|
-
});
|
|
226
|
-
let fields$1 = {
|
|
227
|
-
TAG: "Object",
|
|
228
|
-
_0: [
|
|
229
|
-
[
|
|
230
|
-
"records",
|
|
231
|
-
{
|
|
232
|
-
TAG: "Deserializer",
|
|
233
|
-
_0: List
|
|
234
|
-
}
|
|
235
|
-
],
|
|
236
|
-
[
|
|
237
|
-
"next",
|
|
238
|
-
{
|
|
239
|
-
TAG: "Optional",
|
|
240
|
-
_0: "Self"
|
|
241
|
-
}
|
|
242
|
-
]
|
|
243
|
-
]
|
|
244
|
-
};
|
|
245
|
-
let Ledger = Deser.MakeDeserializer({
|
|
246
|
-
fields: fields$1
|
|
247
|
-
});
|
|
248
241
|
let data = {"records": {"head": "A", "tail": {"head": "B"}},
|
|
249
242
|
"next": {"records": {"head": "A", "tail": {"head": "B"}}}};
|
|
250
243
|
let expected = {
|
|
@@ -267,7 +260,7 @@ Qunit.module("Recursive deserializer", param => {
|
|
|
267
260
|
}
|
|
268
261
|
};
|
|
269
262
|
qunit.expect(1);
|
|
270
|
-
qunit.deepEqual(
|
|
263
|
+
qunit.deepEqual(Deserializer$2.fromJSON(data), {
|
|
271
264
|
TAG: "Ok",
|
|
272
265
|
_0: expected
|
|
273
266
|
}, "nice ledger");
|
|
@@ -297,5 +290,171 @@ Qunit.module("Type safety limits", param => {
|
|
|
297
290
|
});
|
|
298
291
|
});
|
|
299
292
|
|
|
293
|
+
Qunit.module("isSafeCast and toJSON", param => {
|
|
294
|
+
let fields = {
|
|
295
|
+
TAG: "Object",
|
|
296
|
+
_0: [
|
|
297
|
+
[
|
|
298
|
+
"name",
|
|
299
|
+
"String"
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
"count",
|
|
303
|
+
"Int"
|
|
304
|
+
],
|
|
305
|
+
[
|
|
306
|
+
"ratio",
|
|
307
|
+
"Float"
|
|
308
|
+
],
|
|
309
|
+
[
|
|
310
|
+
"active",
|
|
311
|
+
"Boolean"
|
|
312
|
+
]
|
|
313
|
+
]
|
|
314
|
+
};
|
|
315
|
+
let Deserializer$3 = Deser.MakeDeserializer({
|
|
316
|
+
fields: fields
|
|
317
|
+
});
|
|
318
|
+
let fields$1 = {
|
|
319
|
+
TAG: "Object",
|
|
320
|
+
_0: [
|
|
321
|
+
[
|
|
322
|
+
"items",
|
|
323
|
+
{
|
|
324
|
+
TAG: "Array",
|
|
325
|
+
_0: "String"
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
[
|
|
329
|
+
"tags",
|
|
330
|
+
{
|
|
331
|
+
TAG: "Mapping",
|
|
332
|
+
_0: "Int"
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
[
|
|
336
|
+
"label",
|
|
337
|
+
{
|
|
338
|
+
TAG: "Optional",
|
|
339
|
+
_0: "String"
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
let Deserializer$4 = Deser.MakeDeserializer({
|
|
345
|
+
fields: fields$1
|
|
346
|
+
});
|
|
347
|
+
let fields$2 = {
|
|
348
|
+
TAG: "Object",
|
|
349
|
+
_0: [
|
|
350
|
+
[
|
|
351
|
+
"name",
|
|
352
|
+
"String"
|
|
353
|
+
],
|
|
354
|
+
[
|
|
355
|
+
"created",
|
|
356
|
+
"Date"
|
|
357
|
+
]
|
|
358
|
+
]
|
|
359
|
+
};
|
|
360
|
+
let Deserializer$5 = Deser.MakeDeserializer({
|
|
361
|
+
fields: fields$2
|
|
362
|
+
});
|
|
363
|
+
let fields$3 = {
|
|
364
|
+
TAG: "Object",
|
|
365
|
+
_0: [[
|
|
366
|
+
"event",
|
|
367
|
+
{
|
|
368
|
+
TAG: "Deserializer",
|
|
369
|
+
_0: Deserializer$5
|
|
370
|
+
}
|
|
371
|
+
]]
|
|
372
|
+
};
|
|
373
|
+
let Deserializer$6 = Deser.MakeDeserializer({
|
|
374
|
+
fields: fields$3
|
|
375
|
+
});
|
|
376
|
+
Qunit.test("Safe types: primitives return true for isSafeCast", qunit => {
|
|
377
|
+
qunit.expect(1);
|
|
378
|
+
qunit.true(Deserializer$3.isSafeCast(), "primitives are safe");
|
|
379
|
+
});
|
|
380
|
+
Qunit.test("Safe types: nested arrays/mappings/optionals return true for isSafeCast", qunit => {
|
|
381
|
+
qunit.expect(1);
|
|
382
|
+
qunit.true(Deserializer$4.isSafeCast(), "nested safe types are safe");
|
|
383
|
+
});
|
|
384
|
+
Qunit.test("Unsafe types: Date returns false for isSafeCast", qunit => {
|
|
385
|
+
qunit.expect(1);
|
|
386
|
+
qunit.false(Deserializer$5.isSafeCast(), "Date is unsafe");
|
|
387
|
+
});
|
|
388
|
+
Qunit.test("Unsafe types: nested deserializer with Date returns false", qunit => {
|
|
389
|
+
qunit.expect(1);
|
|
390
|
+
qunit.false(Deserializer$6.isSafeCast(), "nested unsafe is unsafe");
|
|
391
|
+
});
|
|
392
|
+
Qunit.test("Round-trip for safe types", qunit => {
|
|
393
|
+
let json = {"name": "test", "count": 42, "ratio": 3.14, "active": true};
|
|
394
|
+
let value = Deserializer$3.fromJSON(json);
|
|
395
|
+
if (value.TAG === "Ok") {
|
|
396
|
+
let serialized = Deserializer$3.toJSON(value._0);
|
|
397
|
+
qunit.deepEqual(serialized, json, "round-trip preserves data");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
qunit.false(true, value._0);
|
|
401
|
+
});
|
|
402
|
+
Qunit.test("Round-trip for unsafe types with Date", qunit => {
|
|
403
|
+
let json = {"name": "event", "created": "2024-01-15T10:30:00.000Z"};
|
|
404
|
+
let value = Deserializer$5.fromJSON(json);
|
|
405
|
+
if (value.TAG === "Ok") {
|
|
406
|
+
let serialized = Deserializer$5.toJSON(value._0);
|
|
407
|
+
qunit.deepEqual(serialized, json, "round-trip preserves data with Date");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
qunit.false(true, value._0);
|
|
411
|
+
});
|
|
412
|
+
Qunit.test("Nested deserializer safety propagation", qunit => {
|
|
413
|
+
let json = {"event": {"name": "meeting", "created": "2024-06-01T09:00:00.000Z"}};
|
|
414
|
+
let value = Deserializer$6.fromJSON(json);
|
|
415
|
+
if (value.TAG === "Ok") {
|
|
416
|
+
let serialized = Deserializer$6.toJSON(value._0);
|
|
417
|
+
qunit.deepEqual(serialized, json, "nested unsafe round-trip works");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
qunit.false(true, value._0);
|
|
421
|
+
});
|
|
422
|
+
Qunit.test("Morphism of safe type is safe", qunit => {
|
|
423
|
+
let fields_1 = v => v;
|
|
424
|
+
let fields = {
|
|
425
|
+
TAG: "Morphism",
|
|
426
|
+
_0: "Int",
|
|
427
|
+
_1: fields_1
|
|
428
|
+
};
|
|
429
|
+
let WithMorphism = Deser.MakeDeserializer({
|
|
430
|
+
fields: fields
|
|
431
|
+
});
|
|
432
|
+
qunit.expect(1);
|
|
433
|
+
qunit.true(WithMorphism.isSafeCast(), "morphism of Int is safe");
|
|
434
|
+
});
|
|
435
|
+
Qunit.test("List deserializer is safe (recursive with Self)", qunit => {
|
|
436
|
+
qunit.expect(1);
|
|
437
|
+
qunit.true(Deserializer$1.isSafeCast(), "recursive list with strings is safe");
|
|
438
|
+
});
|
|
439
|
+
Qunit.test("Appointment deserializer is unsafe (has Date)", qunit => {
|
|
440
|
+
qunit.expect(1);
|
|
441
|
+
qunit.false(Deserializer.isSafeCast(), "Appointment with Date is unsafe");
|
|
442
|
+
});
|
|
443
|
+
Qunit.test("toJSON serializes Date to ISO string", qunit => {
|
|
444
|
+
let appointment_date = new Date("2024-03-15T14:30:00.000Z");
|
|
445
|
+
let appointment_extra = "extra info";
|
|
446
|
+
let appointment = {
|
|
447
|
+
note: "Test",
|
|
448
|
+
date: appointment_date,
|
|
449
|
+
extra: appointment_extra
|
|
450
|
+
};
|
|
451
|
+
let json = Deserializer.toJSON(appointment);
|
|
452
|
+
let expected = {"note": "Test", "date": "2024-03-15T14:30:00.000Z", "extra": "extra info"};
|
|
453
|
+
qunit.deepEqual(json, expected, "Date serialized to ISO string");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
300
457
|
exports.Appointment = Appointment;
|
|
458
|
+
exports.List = List;
|
|
459
|
+
exports.Ledger = Ledger;
|
|
301
460
|
/* Deserializer Not a pure module */
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1769849605835)
|
|
2
|
+
#Done(1769849605836)
|
package/lib/ocaml/Deser.ast
CHANGED
|
Binary file
|
package/lib/ocaml/Deser.cmi
CHANGED
|
Binary file
|
package/lib/ocaml/Deser.cmj
CHANGED
|
Binary file
|
package/lib/ocaml/Deser.cmt
CHANGED
|
Binary file
|
package/lib/ocaml/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/lib/ocaml/index.ast
CHANGED
|
Binary file
|
package/lib/ocaml/index.cmi
CHANGED
|
Binary file
|
package/lib/ocaml/index.cmj
CHANGED
|
Binary file
|
package/lib/ocaml/index.cmt
CHANGED
|
Binary file
|