@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.
@@ -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(List.checkFieldsSanity(), {
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(Ledger.fromJSON(data), {
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 */
@@ -1,2 +1,2 @@
1
- #Start(1766181917581)
2
- #Done(1766181917581)
1
+ #Start(1769849605835)
2
+ #Done(1769849605836)
Binary file
Binary file
Binary file
Binary file
@@ -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
- %%private(let (loc, _f) = __LOC_OF__(module(S: Serializable)))
380
+ let (loc, _f) = __LOC_OF__(module(S: Serializable))
319
381
  let name = `Deserializer ${__MODULE__}, ${loc}`
320
382
 
321
- %%private(external _toNativeType: FieldValue.t => t = "%identity")
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 `'a` into `result<t, string>`")
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
  }
Binary file
Binary file
Binary file
Binary file