@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.
@@ -429,6 +429,97 @@ function checkFieldsSanity(name, _fields, _optional) {
429
429
  };
430
430
  }
431
431
 
432
+ function isSafeCast(_field) {
433
+ while (true) {
434
+ let field = _field;
435
+ if (typeof field !== "object") {
436
+ switch (field) {
437
+ case "Date" :
438
+ case "Datetime" :
439
+ return false;
440
+ default:
441
+ return true;
442
+ }
443
+ } else {
444
+ switch (field.TAG) {
445
+ case "Tuple" :
446
+ return field._0.every(isSafeCast);
447
+ case "Object" :
448
+ return field._0.every(param => isSafeCast(param[1]));
449
+ case "Deserializer" :
450
+ case "Collection" :
451
+ return field._0.isSafeCast();
452
+ case "Array" :
453
+ case "Optional" :
454
+ case "OptionalWithDefault" :
455
+ case "Mapping" :
456
+ case "DefaultWhenInvalid" :
457
+ case "Morphism" :
458
+ _field = field._0;
459
+ continue;
460
+ default:
461
+ return true;
462
+ }
463
+ }
464
+ };
465
+ }
466
+
467
+ function serialize(_value, _shape, self) {
468
+ while (true) {
469
+ let shape = _shape;
470
+ let value = _value;
471
+ if (typeof shape !== "object") {
472
+ switch (shape) {
473
+ case "Date" :
474
+ case "Datetime" :
475
+ return value.toISOString();
476
+ case "Self" :
477
+ _shape = self;
478
+ continue;
479
+ default:
480
+ return value;
481
+ }
482
+ } else {
483
+ switch (shape.TAG) {
484
+ case "Literal" :
485
+ return shape._0;
486
+ case "Array" :
487
+ let inner = shape._0;
488
+ return value.map(v => serialize(v, inner, self));
489
+ case "Tuple" :
490
+ return Belt_Array.zipBy(value, shape._0, (v, f) => serialize(v, f, self));
491
+ case "Object" :
492
+ return Object.fromEntries(Stdlib_Array.filterMap(shape._0, param => {
493
+ let fieldType = param[1];
494
+ let key = param[0];
495
+ return Stdlib_Option.map(value[key], fieldValue => [
496
+ key,
497
+ serialize(fieldValue, fieldType, self)
498
+ ]);
499
+ }));
500
+ case "Optional" :
501
+ if (value === undefined) {
502
+ return null;
503
+ }
504
+ _shape = shape._0;
505
+ _value = Primitive_option.valFromOption(value);
506
+ continue;
507
+ case "Mapping" :
508
+ let inner$1 = shape._0;
509
+ return Stdlib_Dict.mapValues(value, v => serialize(v, inner$1, self));
510
+ case "Deserializer" :
511
+ case "Collection" :
512
+ return shape._0.toJSON(value);
513
+ case "OptionalWithDefault" :
514
+ case "DefaultWhenInvalid" :
515
+ case "Morphism" :
516
+ _shape = shape._0;
517
+ continue;
518
+ }
519
+ }
520
+ };
521
+ }
522
+
432
523
  let Field = {
433
524
  usingString: usingString,
434
525
  usingInt: usingInt,
@@ -442,12 +533,26 @@ let Field = {
442
533
  _taggedToString: _taggedToString,
443
534
  extractValue: extractValue,
444
535
  fromUntagged: fromUntagged,
445
- checkFieldsSanity: checkFieldsSanity
536
+ checkFieldsSanity: checkFieldsSanity,
537
+ isSafeCast: isSafeCast,
538
+ serialize: serialize
446
539
  };
447
540
 
448
541
  function MakeDeserializer(S) {
449
542
  let fields = S.fields;
450
- let name = `Deserializer ` + "Deser" + `, ` + "File \"Deser.res\", line 318, characters 39-47";
543
+ let name = `Deserializer ` + "Deser" + `, ` + "File \"Deser.res\", line 380, characters 29-37";
544
+ let safeCastCache = {
545
+ contents: undefined
546
+ };
547
+ let isSafeCast$1 = () => {
548
+ let cached = safeCastCache.contents;
549
+ if (cached !== undefined) {
550
+ return cached;
551
+ }
552
+ let result = isSafeCast(fields);
553
+ safeCastCache.contents = result;
554
+ return result;
555
+ };
451
556
  let checkFieldsSanity$1 = () => checkFieldsSanity(name, fields, false);
452
557
  let fromJSON = json => {
453
558
  let res;
@@ -468,10 +573,19 @@ function MakeDeserializer(S) {
468
573
  _0: res
469
574
  };
470
575
  };
576
+ let toJSON = value => {
577
+ if (isSafeCast$1()) {
578
+ return value;
579
+ } else {
580
+ return serialize(value, fields, fields);
581
+ }
582
+ };
471
583
  return {
472
584
  name: name,
473
585
  fromJSON: fromJSON,
474
- checkFieldsSanity: checkFieldsSanity$1
586
+ toJSON: toJSON,
587
+ checkFieldsSanity: checkFieldsSanity$1,
588
+ isSafeCast: isSafeCast$1
475
589
  };
476
590
  }
477
591
 
@@ -34,6 +34,59 @@ let Appointment = {
34
34
  Deserializer: Deserializer
35
35
  };
36
36
 
37
+ let fields$1 = {
38
+ TAG: "Object",
39
+ _0: [
40
+ [
41
+ "head",
42
+ "String"
43
+ ],
44
+ [
45
+ "tail",
46
+ {
47
+ TAG: "Optional",
48
+ _0: "Self"
49
+ }
50
+ ]
51
+ ]
52
+ };
53
+
54
+ let Deserializer$1 = Deser.MakeDeserializer({
55
+ fields: fields$1
56
+ });
57
+
58
+ let List = {
59
+ Deserializer: Deserializer$1
60
+ };
61
+
62
+ let fields$2 = {
63
+ TAG: "Object",
64
+ _0: [
65
+ [
66
+ "records",
67
+ {
68
+ TAG: "Deserializer",
69
+ _0: Deserializer$1
70
+ }
71
+ ],
72
+ [
73
+ "next",
74
+ {
75
+ TAG: "Optional",
76
+ _0: "Self"
77
+ }
78
+ ]
79
+ ]
80
+ };
81
+
82
+ let Deserializer$2 = Deser.MakeDeserializer({
83
+ fields: fields$2
84
+ });
85
+
86
+ let Ledger = {
87
+ Deserializer: Deserializer$2
88
+ };
89
+
37
90
  Qunit.module("Basic deserializer", param => {
38
91
  let valid = [
39
92
  [
@@ -164,27 +217,8 @@ Qunit.module("Recursive deserializer", param => {
164
217
  qunit.deepEqual(Stdlib_Result.isError(InfiniteList.checkFieldsSanity()), true, "Ok");
165
218
  });
166
219
  Qunit.test("Finite list", qunit => {
167
- let fields = {
168
- TAG: "Object",
169
- _0: [
170
- [
171
- "head",
172
- "String"
173
- ],
174
- [
175
- "tail",
176
- {
177
- TAG: "Optional",
178
- _0: "Self"
179
- }
180
- ]
181
- ]
182
- };
183
- let List = Deser.MakeDeserializer({
184
- fields: fields
185
- });
186
220
  qunit.expect(1);
187
- qunit.deepEqual(List.checkFieldsSanity(), {
221
+ qunit.deepEqual(Deserializer$1.checkFieldsSanity(), {
188
222
  TAG: "Ok",
189
223
  _0: undefined
190
224
  }, "Ok");
@@ -203,47 +237,6 @@ Qunit.module("Recursive deserializer", param => {
203
237
  });
204
238
  });
205
239
  Qunit.test("Recursion in sub-deserializer", qunit => {
206
- let fields = {
207
- TAG: "Object",
208
- _0: [
209
- [
210
- "head",
211
- "String"
212
- ],
213
- [
214
- "tail",
215
- {
216
- TAG: "Optional",
217
- _0: "Self"
218
- }
219
- ]
220
- ]
221
- };
222
- let List = Deser.MakeDeserializer({
223
- fields: fields
224
- });
225
- let fields$1 = {
226
- TAG: "Object",
227
- _0: [
228
- [
229
- "records",
230
- {
231
- TAG: "Deserializer",
232
- _0: List
233
- }
234
- ],
235
- [
236
- "next",
237
- {
238
- TAG: "Optional",
239
- _0: "Self"
240
- }
241
- ]
242
- ]
243
- };
244
- let Ledger = Deser.MakeDeserializer({
245
- fields: fields$1
246
- });
247
240
  let data = {"records": {"head": "A", "tail": {"head": "B"}},
248
241
  "next": {"records": {"head": "A", "tail": {"head": "B"}}}};
249
242
  let expected = {
@@ -266,7 +259,7 @@ Qunit.module("Recursive deserializer", param => {
266
259
  }
267
260
  };
268
261
  qunit.expect(1);
269
- qunit.deepEqual(Ledger.fromJSON(data), {
262
+ qunit.deepEqual(Deserializer$2.fromJSON(data), {
270
263
  TAG: "Ok",
271
264
  _0: expected
272
265
  }, "nice ledger");
@@ -296,7 +289,173 @@ Qunit.module("Type safety limits", param => {
296
289
  });
297
290
  });
298
291
 
292
+ Qunit.module("isSafeCast and toJSON", param => {
293
+ let fields = {
294
+ TAG: "Object",
295
+ _0: [
296
+ [
297
+ "name",
298
+ "String"
299
+ ],
300
+ [
301
+ "count",
302
+ "Int"
303
+ ],
304
+ [
305
+ "ratio",
306
+ "Float"
307
+ ],
308
+ [
309
+ "active",
310
+ "Boolean"
311
+ ]
312
+ ]
313
+ };
314
+ let Deserializer$3 = Deser.MakeDeserializer({
315
+ fields: fields
316
+ });
317
+ let fields$1 = {
318
+ TAG: "Object",
319
+ _0: [
320
+ [
321
+ "items",
322
+ {
323
+ TAG: "Array",
324
+ _0: "String"
325
+ }
326
+ ],
327
+ [
328
+ "tags",
329
+ {
330
+ TAG: "Mapping",
331
+ _0: "Int"
332
+ }
333
+ ],
334
+ [
335
+ "label",
336
+ {
337
+ TAG: "Optional",
338
+ _0: "String"
339
+ }
340
+ ]
341
+ ]
342
+ };
343
+ let Deserializer$4 = Deser.MakeDeserializer({
344
+ fields: fields$1
345
+ });
346
+ let fields$2 = {
347
+ TAG: "Object",
348
+ _0: [
349
+ [
350
+ "name",
351
+ "String"
352
+ ],
353
+ [
354
+ "created",
355
+ "Date"
356
+ ]
357
+ ]
358
+ };
359
+ let Deserializer$5 = Deser.MakeDeserializer({
360
+ fields: fields$2
361
+ });
362
+ let fields$3 = {
363
+ TAG: "Object",
364
+ _0: [[
365
+ "event",
366
+ {
367
+ TAG: "Deserializer",
368
+ _0: Deserializer$5
369
+ }
370
+ ]]
371
+ };
372
+ let Deserializer$6 = Deser.MakeDeserializer({
373
+ fields: fields$3
374
+ });
375
+ Qunit.test("Safe types: primitives return true for isSafeCast", qunit => {
376
+ qunit.expect(1);
377
+ qunit.true(Deserializer$3.isSafeCast(), "primitives are safe");
378
+ });
379
+ Qunit.test("Safe types: nested arrays/mappings/optionals return true for isSafeCast", qunit => {
380
+ qunit.expect(1);
381
+ qunit.true(Deserializer$4.isSafeCast(), "nested safe types are safe");
382
+ });
383
+ Qunit.test("Unsafe types: Date returns false for isSafeCast", qunit => {
384
+ qunit.expect(1);
385
+ qunit.false(Deserializer$5.isSafeCast(), "Date is unsafe");
386
+ });
387
+ Qunit.test("Unsafe types: nested deserializer with Date returns false", qunit => {
388
+ qunit.expect(1);
389
+ qunit.false(Deserializer$6.isSafeCast(), "nested unsafe is unsafe");
390
+ });
391
+ Qunit.test("Round-trip for safe types", qunit => {
392
+ let json = {"name": "test", "count": 42, "ratio": 3.14, "active": true};
393
+ let value = Deserializer$3.fromJSON(json);
394
+ if (value.TAG === "Ok") {
395
+ let serialized = Deserializer$3.toJSON(value._0);
396
+ qunit.deepEqual(serialized, json, "round-trip preserves data");
397
+ return;
398
+ }
399
+ qunit.false(true, value._0);
400
+ });
401
+ Qunit.test("Round-trip for unsafe types with Date", qunit => {
402
+ let json = {"name": "event", "created": "2024-01-15T10:30:00.000Z"};
403
+ let value = Deserializer$5.fromJSON(json);
404
+ if (value.TAG === "Ok") {
405
+ let serialized = Deserializer$5.toJSON(value._0);
406
+ qunit.deepEqual(serialized, json, "round-trip preserves data with Date");
407
+ return;
408
+ }
409
+ qunit.false(true, value._0);
410
+ });
411
+ Qunit.test("Nested deserializer safety propagation", qunit => {
412
+ let json = {"event": {"name": "meeting", "created": "2024-06-01T09:00:00.000Z"}};
413
+ let value = Deserializer$6.fromJSON(json);
414
+ if (value.TAG === "Ok") {
415
+ let serialized = Deserializer$6.toJSON(value._0);
416
+ qunit.deepEqual(serialized, json, "nested unsafe round-trip works");
417
+ return;
418
+ }
419
+ qunit.false(true, value._0);
420
+ });
421
+ Qunit.test("Morphism of safe type is safe", qunit => {
422
+ let fields_1 = v => v;
423
+ let fields = {
424
+ TAG: "Morphism",
425
+ _0: "Int",
426
+ _1: fields_1
427
+ };
428
+ let WithMorphism = Deser.MakeDeserializer({
429
+ fields: fields
430
+ });
431
+ qunit.expect(1);
432
+ qunit.true(WithMorphism.isSafeCast(), "morphism of Int is safe");
433
+ });
434
+ Qunit.test("List deserializer is safe (recursive with Self)", qunit => {
435
+ qunit.expect(1);
436
+ qunit.true(Deserializer$1.isSafeCast(), "recursive list with strings is safe");
437
+ });
438
+ Qunit.test("Appointment deserializer is unsafe (has Date)", qunit => {
439
+ qunit.expect(1);
440
+ qunit.false(Deserializer.isSafeCast(), "Appointment with Date is unsafe");
441
+ });
442
+ Qunit.test("toJSON serializes Date to ISO string", qunit => {
443
+ let appointment_date = new Date("2024-03-15T14:30:00.000Z");
444
+ let appointment_extra = "extra info";
445
+ let appointment = {
446
+ note: "Test",
447
+ date: appointment_date,
448
+ extra: appointment_extra
449
+ };
450
+ let json = Deserializer.toJSON(appointment);
451
+ let expected = {"note": "Test", "date": "2024-03-15T14:30:00.000Z", "extra": "extra info"};
452
+ qunit.deepEqual(json, expected, "Date serialized to ISO string");
453
+ });
454
+ });
455
+
299
456
  export {
300
457
  Appointment,
458
+ List,
459
+ Ledger,
301
460
  }
302
461
  /* Deserializer Not a pure module */
@@ -430,6 +430,97 @@ function checkFieldsSanity(name, _fields, _optional) {
430
430
  };
431
431
  }
432
432
 
433
+ function isSafeCast(_field) {
434
+ while (true) {
435
+ let field = _field;
436
+ if (typeof field !== "object") {
437
+ switch (field) {
438
+ case "Date" :
439
+ case "Datetime" :
440
+ return false;
441
+ default:
442
+ return true;
443
+ }
444
+ } else {
445
+ switch (field.TAG) {
446
+ case "Tuple" :
447
+ return field._0.every(isSafeCast);
448
+ case "Object" :
449
+ return field._0.every(param => isSafeCast(param[1]));
450
+ case "Deserializer" :
451
+ case "Collection" :
452
+ return field._0.isSafeCast();
453
+ case "Array" :
454
+ case "Optional" :
455
+ case "OptionalWithDefault" :
456
+ case "Mapping" :
457
+ case "DefaultWhenInvalid" :
458
+ case "Morphism" :
459
+ _field = field._0;
460
+ continue;
461
+ default:
462
+ return true;
463
+ }
464
+ }
465
+ };
466
+ }
467
+
468
+ function serialize(_value, _shape, self) {
469
+ while (true) {
470
+ let shape = _shape;
471
+ let value = _value;
472
+ if (typeof shape !== "object") {
473
+ switch (shape) {
474
+ case "Date" :
475
+ case "Datetime" :
476
+ return value.toISOString();
477
+ case "Self" :
478
+ _shape = self;
479
+ continue;
480
+ default:
481
+ return value;
482
+ }
483
+ } else {
484
+ switch (shape.TAG) {
485
+ case "Literal" :
486
+ return shape._0;
487
+ case "Array" :
488
+ let inner = shape._0;
489
+ return value.map(v => serialize(v, inner, self));
490
+ case "Tuple" :
491
+ return Belt_Array.zipBy(value, shape._0, (v, f) => serialize(v, f, self));
492
+ case "Object" :
493
+ return Object.fromEntries(Stdlib_Array.filterMap(shape._0, param => {
494
+ let fieldType = param[1];
495
+ let key = param[0];
496
+ return Stdlib_Option.map(value[key], fieldValue => [
497
+ key,
498
+ serialize(fieldValue, fieldType, self)
499
+ ]);
500
+ }));
501
+ case "Optional" :
502
+ if (value === undefined) {
503
+ return null;
504
+ }
505
+ _shape = shape._0;
506
+ _value = Primitive_option.valFromOption(value);
507
+ continue;
508
+ case "Mapping" :
509
+ let inner$1 = shape._0;
510
+ return Stdlib_Dict.mapValues(value, v => serialize(v, inner$1, self));
511
+ case "Deserializer" :
512
+ case "Collection" :
513
+ return shape._0.toJSON(value);
514
+ case "OptionalWithDefault" :
515
+ case "DefaultWhenInvalid" :
516
+ case "Morphism" :
517
+ _shape = shape._0;
518
+ continue;
519
+ }
520
+ }
521
+ };
522
+ }
523
+
433
524
  let Field = {
434
525
  usingString: usingString,
435
526
  usingInt: usingInt,
@@ -443,12 +534,26 @@ let Field = {
443
534
  _taggedToString: _taggedToString,
444
535
  extractValue: extractValue,
445
536
  fromUntagged: fromUntagged,
446
- checkFieldsSanity: checkFieldsSanity
537
+ checkFieldsSanity: checkFieldsSanity,
538
+ isSafeCast: isSafeCast,
539
+ serialize: serialize
447
540
  };
448
541
 
449
542
  function MakeDeserializer(S) {
450
543
  let fields = S.fields;
451
- let name = `Deserializer ` + "Deser" + `, ` + "File \"Deser.res\", line 318, characters 39-47";
544
+ let name = `Deserializer ` + "Deser" + `, ` + "File \"Deser.res\", line 380, characters 29-37";
545
+ let safeCastCache = {
546
+ contents: undefined
547
+ };
548
+ let isSafeCast$1 = () => {
549
+ let cached = safeCastCache.contents;
550
+ if (cached !== undefined) {
551
+ return cached;
552
+ }
553
+ let result = isSafeCast(fields);
554
+ safeCastCache.contents = result;
555
+ return result;
556
+ };
452
557
  let checkFieldsSanity$1 = () => checkFieldsSanity(name, fields, false);
453
558
  let fromJSON = json => {
454
559
  let res;
@@ -469,10 +574,19 @@ function MakeDeserializer(S) {
469
574
  _0: res
470
575
  };
471
576
  };
577
+ let toJSON = value => {
578
+ if (isSafeCast$1()) {
579
+ return value;
580
+ } else {
581
+ return serialize(value, fields, fields);
582
+ }
583
+ };
472
584
  return {
473
585
  name: name,
474
586
  fromJSON: fromJSON,
475
- checkFieldsSanity: checkFieldsSanity$1
587
+ toJSON: toJSON,
588
+ checkFieldsSanity: checkFieldsSanity$1,
589
+ isSafeCast: isSafeCast$1
476
590
  };
477
591
  }
478
592