@kaiko.io/rescript-deser 7.0.0-alpha.1 → 7.0.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.
Files changed (61) hide show
  1. package/README.md +13 -2
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/build.ninja +0 -27
  4. package/lib/bs/compiler-info.json +8 -0
  5. package/lib/bs/src/Deser.ast +0 -0
  6. package/lib/bs/src/Deser.cmi +0 -0
  7. package/lib/bs/src/Deser.cmj +0 -0
  8. package/lib/bs/src/Deser.cmt +0 -0
  9. package/lib/bs/{___incremental → src}/Deser.res +19 -29
  10. package/lib/bs/tests/QUnit.ast +0 -0
  11. package/lib/bs/tests/QUnit.cmi +0 -0
  12. package/lib/bs/tests/QUnit.cmj +0 -0
  13. package/lib/bs/tests/QUnit.cmt +0 -0
  14. package/lib/bs/tests/QUnit.res +72 -0
  15. package/lib/bs/tests/index.ast +0 -0
  16. package/lib/bs/tests/index.cmi +0 -0
  17. package/lib/bs/tests/index.cmj +0 -0
  18. package/lib/bs/tests/index.cmt +0 -0
  19. package/lib/bs/{___incremental → tests}/index.res +24 -1
  20. package/lib/es6/src/Deser.js +297 -365
  21. package/lib/es6/tests/index.js +260 -241
  22. package/lib/js/src/Deser.js +293 -361
  23. package/lib/js/tests/index.js +261 -242
  24. package/lib/ocaml/.compiler.log +2 -0
  25. package/lib/ocaml/Deser.ast +0 -0
  26. package/lib/ocaml/Deser.cmi +0 -0
  27. package/lib/ocaml/Deser.cmj +0 -0
  28. package/lib/ocaml/Deser.cmt +0 -0
  29. package/lib/ocaml/Deser.res +341 -0
  30. package/lib/ocaml/QUnit.ast +0 -0
  31. package/lib/ocaml/QUnit.cmi +0 -0
  32. package/lib/ocaml/QUnit.cmj +0 -0
  33. package/lib/ocaml/QUnit.cmt +0 -0
  34. package/lib/ocaml/QUnit.res +72 -0
  35. package/lib/ocaml/index.ast +0 -0
  36. package/lib/ocaml/index.cmi +0 -0
  37. package/lib/ocaml/index.cmj +0 -0
  38. package/lib/ocaml/index.cmt +0 -0
  39. package/lib/ocaml/index.res +212 -0
  40. package/lib/rescript.lock +1 -0
  41. package/package.json +4 -6
  42. package/rescript.json +4 -6
  43. package/src/Deser.res +19 -29
  44. package/tests/QUnit.res +4 -4
  45. package/tests/index.res +24 -1
  46. package/yarn.lock +683 -0
  47. package/lib/bs/.bsbuild +0 -0
  48. package/lib/bs/.bsdeps +0 -9
  49. package/lib/bs/.ninja_log +0 -57
  50. package/lib/bs/.project-files-cache +0 -0
  51. package/lib/bs/.sourcedirs.json +0 -1
  52. package/lib/bs/___incremental/Deser.cmi +0 -0
  53. package/lib/bs/___incremental/Deser.cmj +0 -0
  54. package/lib/bs/___incremental/Deser.cmt +0 -0
  55. package/lib/bs/___incremental/index.cmi +0 -0
  56. package/lib/bs/___incremental/index.cmj +0 -0
  57. package/lib/bs/___incremental/index.cmt +0 -0
  58. package/lib/bs/install.ninja +0 -10
  59. package/lib/bs/src/Deser.d +0 -0
  60. package/lib/bs/tests/QUnit.d +0 -0
  61. package/lib/bs/tests/index.d +0 -1
@@ -0,0 +1,341 @@
1
+ module FieldValue = {
2
+ type t
3
+ external string: string => t = "%identity"
4
+ external int: int => t = "%identity"
5
+ external float: float => t = "%identity"
6
+ external boolean: bool => t = "%identity"
7
+ external array: array<t> => t = "%identity"
8
+ external object: dict<t> => t = "%identity"
9
+ external mapping: dict<t> => t = "%identity"
10
+ external any: 'a => t = "%identity"
11
+ @val external null: t = "undefined"
12
+
13
+ external asString: t => string = "%identity"
14
+ external asInt: t => int = "%identity"
15
+ external asFloat: t => float = "%identity"
16
+ external asBoolean: t => bool = "%identity"
17
+ external asArray: t => array<'a> = "%identity"
18
+ external asObject: t => 'a = "%identity"
19
+ }
20
+
21
+ exception TypeError(string)
22
+
23
+ @doc("The module type of a built deserializer which is suitable to add as a subparser.")
24
+ module type Deserializer = {
25
+ type t
26
+ let name: string
27
+ let fromJSON: JSON.t => result<t, string>
28
+ let checkFieldsSanity: unit => result<unit, string>
29
+ }
30
+
31
+ module Field = {
32
+ type rec t =
33
+ | Any
34
+ | String
35
+ | Literal(string)
36
+ | Int
37
+ | Float
38
+ | Boolean
39
+ | Array(t)
40
+ // These SHOULD strings in ISO format, but we only validate the string
41
+ // can be represented in Js.Date without spewing NaN all over the place;
42
+ // Js.Date.fromString("xxx") returns an object that is mostly unusable.
43
+ ///
44
+ // We also allow floats and then use Date.fromTime
45
+ | Date
46
+ | Datetime // alias of Date
47
+ | Tuple(array<t>)
48
+ | Object(array<(string, t)>)
49
+ | Optional(t)
50
+ | OptionalWithDefault(t, FieldValue.t)
51
+ // An arbitrary mapping from names to other arbitrary fields. The
52
+ // difference with Object, is that you don't know the names of the
53
+ // expected entries.
54
+ | Mapping(t)
55
+ | Deserializer(module(Deserializer))
56
+ // A specialized Array of deserialized items that ignores unparsable
57
+ // items and returns the valid collection. This saves the user from
58
+ // writing 'Array(DefaultWhenInvalid(Optional(Deserializer(module(M)))))'
59
+ // and then post-process the list of items with 'Array.keepSome'
60
+ | Collection(module(Deserializer))
61
+ | DefaultWhenInvalid(t, FieldValue.t)
62
+ // FIXME: this is used to add additional restrictions like variadictInt or
63
+ // variadicString; but I find it too type-unsafe. I might consider having
64
+ // a Constraints for this in the future.
65
+ | Morphism(t, FieldValue.t => FieldValue.t)
66
+ | Self
67
+
68
+ let usingString = (f: string => 'a) => value => value->FieldValue.asString->f->FieldValue.any
69
+ let usingInt = (f: int => 'a) => value => value->FieldValue.asInt->f->FieldValue.any
70
+ let usingFloat = (f: float => 'a) => value => value->FieldValue.asFloat->f->FieldValue.any
71
+ let usingBoolean = (f: bool => 'a) => value => value->FieldValue.asBoolean->f->FieldValue.any
72
+ let usingArray = (f: array<'a> => 'b) => value => value->FieldValue.asArray->f->FieldValue.any
73
+ let usingObject = (f: 'a => 'b) => value => value->FieldValue.asObject->f->FieldValue.any
74
+
75
+ let variadicInt = (hint: string, fromJs: int => option<'variadicType>) => Morphism(
76
+ Int,
77
+ usingInt(i => {
78
+ switch i->fromJs {
79
+ | Some(internalValue) => internalValue
80
+ | None =>
81
+ throw(TypeError(`This Int(${i->Int.toString}) not a valid value here. Hint: ${hint}`))
82
+ }
83
+ }),
84
+ )
85
+ let variadicString = (hint: string, fromJs: string => option<'variadicType>) => Morphism(
86
+ String,
87
+ usingString(i => {
88
+ switch i->fromJs {
89
+ | Some(internalValue) => internalValue
90
+ | None => throw(TypeError(`This String("${i}") not a valid value here. Hint: ${hint}`))
91
+ }
92
+ }),
93
+ )
94
+
95
+ let rec toString = (type_: t) =>
96
+ switch type_ {
97
+ | Any => "Any"
98
+ | String => "String"
99
+ | Literal(lit) => `Literal: ${lit}`
100
+ | Int => "Integer"
101
+ | Float => "Float"
102
+ | Boolean => "Boolean"
103
+ | Datetime
104
+ | Date => "Date"
105
+ | Self => "Self (recursive)"
106
+ | Collection(m) => {
107
+ module M = unpack(m: Deserializer)
108
+ "Collection of " ++ M.name
109
+ }
110
+
111
+ | Array(t) => "Array of " ++ t->toString
112
+ | Tuple(bases) => `Tuple of (${bases->Array.map(toString)->Array.join(", ")})`
113
+ | Object(fields) => {
114
+ let desc = fields->Array.map(((field, t)) => `${field}: ${t->toString}`)->Array.join(", ")
115
+ `Object of {${desc}}`
116
+ }
117
+
118
+ | OptionalWithDefault(t, _)
119
+ | Optional(t) =>
120
+ "Null of " ++ t->toString
121
+ | Mapping(t) => `Mapping of ${t->toString}`
122
+ | Morphism(t, _) => t->toString ++ " to apply a morphism"
123
+ | Deserializer(m) => {
124
+ module M = unpack(m: Deserializer)
125
+ M.name
126
+ }
127
+
128
+ | DefaultWhenInvalid(t, _) => `Protected ${t->toString}`
129
+ }
130
+
131
+ let _taggedToString = (tagged: JSON.t) => {
132
+ switch tagged {
133
+ | Boolean(false) => "Boolean(false)"
134
+ | Boolean(true) => "Boolean(true)"
135
+ | Null => "Null"
136
+ | String(text) => `String("${text}")`
137
+ | Number(number) => `Number(${number->Float.toString})`
138
+ | Object(obj) => `Object(${obj->JSON.stringifyAny->Option.getOr("...")})`
139
+ | Array(array) => `Array(${array->JSON.stringifyAny->Option.getOr("...")})`
140
+ }
141
+ }
142
+
143
+ let rec extractValue = (values: dict<JSON.t>, field: string, shape: t, self: t): FieldValue.t => {
144
+ switch values->Dict.get(field) {
145
+ | Some(value) => value->fromUntagged(shape, self)
146
+ | None =>
147
+ switch shape {
148
+ | DefaultWhenInvalid(_, _) => JSON.Null->fromUntagged(shape, self)
149
+ | Optional(_) => JSON.Null->fromUntagged(shape, self)
150
+ | OptionalWithDefault(_, default) => default
151
+ | _ => throw(TypeError(`Missing non-optional field '${field}'`))
152
+ }
153
+ }
154
+ }
155
+ and fromUntagged: (JSON.t, t, t) => FieldValue.t = (untagged, shape, self) => {
156
+ switch (shape, untagged) {
157
+ | (Any, _) => untagged->FieldValue.any
158
+
159
+ | (Literal(expected), String(text)) if text == expected => FieldValue.string(text)
160
+ | (Literal(expected), String(text)) =>
161
+ throw(TypeError(`Expecting literal ${expected}, got ${text}`))
162
+
163
+ | (String, String(text)) => FieldValue.string(text)
164
+
165
+ | (Int, Number(number)) => FieldValue.int(number->Float.toInt)
166
+
167
+ | (Float, Number(number)) => FieldValue.float(number)
168
+
169
+ | (Boolean, Boolean(v)) => FieldValue.boolean(v)
170
+
171
+ | (Tuple(bases), Array(items)) => {
172
+ let lenbases = bases->Array.length
173
+ let lenitems = items->Array.length
174
+ if lenbases == lenitems {
175
+ let values = Belt.Array.zipBy(items, bases, (i, b) => fromUntagged(i, b, self))
176
+ values->FieldValue.array
177
+ } else {
178
+ throw(
179
+ TypeError(`Expecting ${lenbases->Int.toString} items, got ${lenitems->Int.toString}`),
180
+ )
181
+ }
182
+ }
183
+
184
+ | (Datetime | Date, String(s)) =>
185
+ let r = Date.fromString(s)
186
+ if r->Date.getDate->Int.toFloat->Float.isNaN {
187
+ throw(TypeError(`Invalid date ${s}`))
188
+ }
189
+ r->FieldValue.any
190
+
191
+ | (Datetime | Date, Number(f)) =>
192
+ let r = Date.fromTime(f)
193
+ if r->Date.getDate->Int.toFloat->Float.isNaN {
194
+ throw(TypeError(`Invalid date ${f->Float.toString}`))
195
+ }
196
+ r->FieldValue.any
197
+
198
+ | (Array(shape), Array(items)) =>
199
+ FieldValue.array(items->Array.map(item => item->fromUntagged(shape, self)))
200
+
201
+ | (Mapping(f), Object(values)) =>
202
+ values->Dict.mapValues(v => v->fromUntagged(f, self))->FieldValue.mapping
203
+
204
+ | (Object(fields), Object(values)) =>
205
+ fields
206
+ ->Array.map(((field, shape)) => {
207
+ let value = switch extractValue(values, field, shape, self) {
208
+ | value => value
209
+ | exception TypeError(msg) => throw(TypeError(`Field "${field}": ${msg}`))
210
+ }
211
+ (field, value)
212
+ })
213
+ ->Dict.fromArray
214
+ ->FieldValue.object
215
+
216
+ | (OptionalWithDefault(_, value), Null) => value
217
+ | (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape, self)
218
+ | (Optional(_), Null) => FieldValue.null
219
+ | (Optional(shape), _) => untagged->fromUntagged(shape, self)
220
+ | (Morphism(shape, f), _) => untagged->fromUntagged(shape, self)->f->FieldValue.any
221
+
222
+ | (Collection(m), Array(items)) => {
223
+ module M = unpack(m: Deserializer)
224
+ items
225
+ ->Array.map(M.fromJSON)
226
+ ->Array.filterMap(x =>
227
+ switch x {
228
+ | Ok(v) => Some(v)
229
+ | Error(msg) =>
230
+ Console.warn3(__MODULE__, "Could not deserialize value in the collection", msg)
231
+ None
232
+ }
233
+ )
234
+ ->Array.map(FieldValue.any)
235
+ ->FieldValue.array
236
+ }
237
+
238
+ | (Deserializer(m), _) => {
239
+ module M = unpack(m: Deserializer)
240
+ switch untagged->M.fromJSON {
241
+ | Ok(res) => res->FieldValue.any
242
+ | Error(msg) => throw(TypeError(msg))
243
+ }
244
+ }
245
+
246
+ | (DefaultWhenInvalid(t, default), _) =>
247
+ switch untagged->fromUntagged(t, self) {
248
+ | res => res
249
+ | exception TypeError(msg) => {
250
+ Console.warn2("Detected and ignore (with default): ", msg)
251
+ default
252
+ }
253
+ }
254
+ | (Self, _) => untagged->fromUntagged(self, self)
255
+ | (expected, actual) =>
256
+ throw(TypeError(`Expected ${expected->toString}, but got ${actual->_taggedToString} instead`))
257
+ }
258
+ }
259
+
260
+ let rec checkFieldsSanity = (name: string, fields: t, optional: bool): result<unit, string> =>
261
+ switch (fields, optional) {
262
+ | (Self, false) => Error(`${name}: Trivial infinite recursion 'let fields = Self'`)
263
+ | (Self, true) => Ok()
264
+
265
+ | (Any, _) => Ok()
266
+ | (String, _) | (Float, _) | (Int, _) | (Literal(_), _) => Ok()
267
+ | (Boolean, _) | (Date, _) | (Datetime, _) => Ok()
268
+ | (Morphism(_, _), _) => Ok()
269
+
270
+ | (Collection(mod), _)
271
+ | (Deserializer(mod), _) => {
272
+ module M = unpack(mod: Deserializer)
273
+ switch M.checkFieldsSanity() {
274
+ | Ok() => Ok()
275
+ | Error(msg) => Error(`${name}/ ${msg}`)
276
+ }
277
+ }
278
+
279
+ | (DefaultWhenInvalid(fields, _), _)
280
+ | (OptionalWithDefault(fields, _), _)
281
+ | (Optional(fields), _) =>
282
+ checkFieldsSanity(name, fields, true)
283
+
284
+ | (Object(fields), optional) =>
285
+ fields
286
+ ->Array.map(((fieldName, field)) =>
287
+ () => checkFieldsSanity(`${name}::${fieldName}`, field, optional)
288
+ )
289
+ ->Array.reduce(Ok([]), (res, nextitem) =>
290
+ res->Result.flatMap(arr => nextitem()->Result.map(i => arr->Array.concat([i])))
291
+ )
292
+ ->Result.map(_ => ())
293
+
294
+ // Mappings and arrays can be empty, so their payloads are
295
+ // automatically optional.
296
+ | (Mapping(field), _) | (Array(field), _) => checkFieldsSanity(name, field, true)
297
+
298
+ | (Tuple(fields), optional) =>
299
+ fields
300
+ ->Array.mapWithIndex((field, index) =>
301
+ () => checkFieldsSanity(`${name}[${index->Int.toString}]`, field, optional)
302
+ )
303
+ ->Array.reduce(Ok([]), (res, nextitem) =>
304
+ res->Result.flatMap(arr => nextitem()->Result.map(i => arr->Array.concat([i])))
305
+ )
306
+ ->Result.map(_ => ())
307
+ }
308
+ }
309
+
310
+ module type Serializable = {
311
+ type t
312
+ let fields: Field.t
313
+ }
314
+
315
+ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) => {
316
+ type t = S.t
317
+ let fields = S.fields
318
+ %%private(let (loc, _f) = __LOC_OF__(module(S: Serializable)))
319
+ let name = `Deserializer ${__MODULE__}, ${loc}`
320
+
321
+ %%private(external _toNativeType: FieldValue.t => t = "%identity")
322
+
323
+ @doc("Checks for trivial infinite-recursion in the fields of the module.
324
+
325
+ Notice this algorithm is just an heuristic, and it might happen that are
326
+ cases of infinite-recursion not detected and cases where detection is a
327
+ false positive.
328
+
329
+ You should use this only while debugging/developing to verify your data.
330
+
331
+ ")
332
+ let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
333
+
334
+ @doc("Parse a `'a` into `result<t, string>`")
335
+ let fromJSON: JSON.t => result<t, _> = json => {
336
+ switch json->Field.fromUntagged(fields, fields) {
337
+ | res => Ok(res->_toNativeType)
338
+ | exception TypeError(e) => Error(e)
339
+ }
340
+ }
341
+ }
Binary file
Binary file
Binary file
Binary file
@@ -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, promise<'a>, message) => unit = "rejects"
52
+ @send external rejectsM: (assertion, promise<'a>, message) => unit = "rejects"
53
+ @send
54
+ external rejectMatches: (assertion, promise<'a>, matcher<'a>, message) => unit = "rejects"
55
+ @send
56
+ external rejectMatchesM: (assertion, promise<'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"
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,212 @@
1
+ open QUnit
2
+
3
+ module Appointment = {
4
+ type t = {
5
+ note: string,
6
+ date: Date.t,
7
+ extra: option<string>,
8
+ }
9
+ module Deserializer = Deser.MakeDeserializer({
10
+ type t = t
11
+ open Deser.Field
12
+
13
+ let fields = Object([("note", String), ("date", Date), ("extra", Optional(String))])
14
+ })
15
+ }
16
+
17
+ module_("Basic deserializer", _ => {
18
+ let valid: array<(_, Appointment.t)> = [
19
+ (
20
+ %raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
21
+ {
22
+ note: "Bicentennial doctor's appointment",
23
+ date: Date.fromString("2100-01-01"),
24
+ extra: None,
25
+ },
26
+ ),
27
+ (
28
+ %raw(`{
29
+ "note": "Bicentennial doctor's appointment",
30
+ "date": "2100-01-01",
31
+ "extra": "Don't take your stop-aging pills the night before the appointment",
32
+ }`),
33
+ {
34
+ note: "Bicentennial doctor's appointment",
35
+ date: Date.fromString("2100-01-01"),
36
+ extra: Some("Don't take your stop-aging pills the night before the appointment"),
37
+ },
38
+ ),
39
+ ]
40
+
41
+ test("Correctly deserializes data", qunit => {
42
+ qunit->expect(valid->Array.length)
43
+ valid->Array.forEach(
44
+ ((data, expected)) => {
45
+ Console.log2("Running sample", data)
46
+ switch Appointment.Deserializer.fromJSON(data) {
47
+ | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
48
+ | Error(msg) => Console.error(msg)
49
+ }
50
+ },
51
+ )
52
+ })
53
+
54
+ let invalid = [
55
+ (
56
+ "Missing non-optional field",
57
+ %raw(`{"extra": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
58
+ ),
59
+ ]
60
+
61
+ test("Correctly catches invalid data", qunit => {
62
+ qunit->expect(invalid->Array.length)
63
+ invalid->Array.forEach(
64
+ ((message, data)) => {
65
+ Console.log3("Running sample", message, data)
66
+ switch Appointment.Deserializer.fromJSON(data) {
67
+ | Ok(result) => Console.error2("Invalid being accepted: ", result)
68
+ | Error(msg) => {
69
+ Console.log2("Correctly detected:", msg)
70
+ qunit->ok(true, true)
71
+ }
72
+ }
73
+ },
74
+ )
75
+ })
76
+ })
77
+
78
+ module_("Recursive deserializer", _ => {
79
+ module TrivialTree = {
80
+ type rec t<'a> = {
81
+ data: 'a,
82
+ children: array<t<'a>>,
83
+ }
84
+
85
+ module DeserializerImpl = Deser.MakeDeserializer({
86
+ type payload
87
+ type rec t = {
88
+ data: payload,
89
+ children: array<t>,
90
+ }
91
+ open Deser.Field
92
+
93
+ let fields = Object([("data", Any), ("children", Array(Self))])
94
+ })
95
+
96
+ module Deserializer = {
97
+ include DeserializerImpl
98
+
99
+ let fromJSON = data => data->DeserializerImpl.fromJSON->Result.map(x => x->Obj.magic)
100
+ }
101
+ }
102
+
103
+ let valid: array<(_, TrivialTree.t<string>)> = [
104
+ (
105
+ %raw(`{"data": "A", "children": [{"data": "A1", "children": []}, {"data": "B", "children": [{"data": "C", "children": []}, {"data": "D", "children": []}]}]}`),
106
+ {
107
+ data: "A",
108
+ children: [
109
+ {data: "A1", children: []},
110
+ {data: "B", children: [{data: "C", children: []}, {data: "D", children: []}]},
111
+ ],
112
+ },
113
+ ),
114
+ ]
115
+
116
+ test("Trivial recursion detection: Ok", qunit => {
117
+ qunit->expect(1)
118
+ qunit->deepEqual(TrivialTree.Deserializer.checkFieldsSanity(), Ok(), "Ok")
119
+ })
120
+
121
+ test("Infinite list", qunit => {
122
+ module InfiniteList = Deser.MakeDeserializer({
123
+ open Deser.Field
124
+
125
+ type t
126
+ let fields = Object([("head", String), ("tail", Self)])
127
+ })
128
+
129
+ qunit->expect(1)
130
+ qunit->deepEqual(InfiniteList.checkFieldsSanity()->Result.isError, true, "Ok")
131
+ })
132
+
133
+ 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
+ qunit->expect(1)
142
+ qunit->deepEqual(List.checkFieldsSanity(), Ok(), "Ok")
143
+ })
144
+
145
+ test("Correctly deserializes recursive data", qunit => {
146
+ qunit->expect(valid->Array.length)
147
+ valid->Array.forEach(
148
+ ((data, expected)) => {
149
+ Console.log2("Running sample", data)
150
+ switch TrivialTree.Deserializer.fromJSON(data) {
151
+ | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
152
+ | Error(msg) => Console.error(msg)
153
+ }
154
+ },
155
+ )
156
+ })
157
+
158
+ 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
+ let data = %raw(`
174
+ {"records": {"head": "A", "tail": {"head": "B"}},
175
+ "next": {"records": {"head": "A", "tail": {"head": "B"}}}}
176
+ `)
177
+ let expected = {
178
+ "records": {"head": "A", "tail": {"head": "B", "tail": None}},
179
+ "next": {
180
+ "records": {"head": "A", "tail": {"head": "B", "tail": None}},
181
+ "next": None,
182
+ },
183
+ }
184
+
185
+ qunit->expect(1)
186
+ qunit->deepEqual(data->Ledger.fromJSON->Obj.magic, Ok(expected), "nice ledger")
187
+ })
188
+ })
189
+
190
+ module_("Type safety limits", _ => {
191
+ test("ill-defined deserializer can cheat the type system", qunit => {
192
+ module X = Deser.MakeDeserializer({
193
+ type t = string
194
+ let fields = Deser.Field.Array(Int)
195
+ })
196
+
197
+ let data = %raw("[1]")
198
+ qunit->throws(
199
+ _ => {
200
+ let illString = data->X.fromJSON->Result.getOr("")
201
+ try {
202
+ Console.info3(__MODULE__, "This will fail with a type error", illString->String.charAt(0))
203
+ } catch {
204
+ | e =>
205
+ Console.error(e)
206
+ throw(e)
207
+ }
208
+ },
209
+ "Expected: TypeError: illString.charAt is not a function",
210
+ )
211
+ })
212
+ })
@@ -0,0 +1 @@
1
+ 1387245
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaiko.io/rescript-deser",
3
- "version": "7.0.0-alpha.1",
3
+ "version": "7.0.0",
4
4
  "keywords": [
5
5
  "json",
6
6
  "deserializer",
@@ -19,17 +19,15 @@
19
19
  "README.md"
20
20
  ],
21
21
  "peerDependencies": {
22
- "@rescript/core": ">=1.6",
23
- "rescript": ">=11.1.0"
22
+ "rescript": ">=12.0.0"
24
23
  },
25
24
  "devDependencies": {
26
25
  "@babel/code-frame": "7.18.6",
27
- "@jihchi/vite-plugin-rescript": "^6.0",
26
+ "@jihchi/vite-plugin-rescript": ">=8.0.0-beta.2",
28
27
  "@playwright/test": "1.56.1",
29
- "@rescript/core": "^1.6.0",
30
28
  "@rescript/tools": "0.6.4",
31
29
  "qunit": "^2.16.0",
32
- "rescript": "^11.1.0",
30
+ "rescript": "^12.0.0",
33
31
  "vite": "^5.4.0"
34
32
  }
35
33
  }