@kaiko.io/rescript-deser 6.0.1 → 7.0.0-alpha.2

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 (56) hide show
  1. package/README.md +16 -5
  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/src/Deser.res +341 -0
  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/tests/index.res +212 -0
  20. package/lib/es6/src/Deser.js +302 -477
  21. package/lib/es6/tests/index.js +260 -241
  22. package/lib/js/src/Deser.js +298 -473
  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 +5 -6
  42. package/rescript.json +4 -6
  43. package/src/Deser.res +86 -97
  44. package/tests/QUnit.res +4 -4
  45. package/tests/index.res +34 -11
  46. package/tests/run-tests.js +192 -0
  47. package/yarn.lock +683 -0
  48. package/lib/bs/.bsbuild +0 -0
  49. package/lib/bs/.bsdeps +0 -9
  50. package/lib/bs/.ninja_log +0 -93
  51. package/lib/bs/.project-files-cache +0 -0
  52. package/lib/bs/.sourcedirs.json +0 -1
  53. package/lib/bs/install.ninja +0 -10
  54. package/lib/bs/src/Deser.d +0 -0
  55. package/lib/bs/tests/QUnit.d +0 -0
  56. package/lib/bs/tests/index.d +0 -1
package/rescript.json CHANGED
@@ -10,16 +10,14 @@
10
10
  "type": "dev"
11
11
  }
12
12
  ],
13
- "bs-dependencies": [
14
- "@rescript/core"
15
- ],
16
- "uncurried": true,
17
13
  "package-specs": [
18
14
  {
19
- "module": "esmodule"
15
+ "module": "esmodule",
16
+ "in-source": false
20
17
  },
21
18
  {
22
- "module": "commonjs"
19
+ "module": "commonjs",
20
+ "in-source": false
23
21
  }
24
22
  ],
25
23
  "warnings": {
package/src/Deser.res CHANGED
@@ -1,5 +1,3 @@
1
- open RescriptCore
2
-
3
1
  module FieldValue = {
4
2
  type t
5
3
  external string: string => t = "%identity"
@@ -7,8 +5,8 @@ module FieldValue = {
7
5
  external float: float => t = "%identity"
8
6
  external boolean: bool => t = "%identity"
9
7
  external array: array<t> => t = "%identity"
10
- external object: Dict.t<t> => t = "%identity"
11
- external mapping: Dict.t<t> => t = "%identity"
8
+ external object: dict<t> => t = "%identity"
9
+ external mapping: dict<t> => t = "%identity"
12
10
  external any: 'a => t = "%identity"
13
11
  @val external null: t = "undefined"
14
12
 
@@ -26,7 +24,7 @@ exception TypeError(string)
26
24
  module type Deserializer = {
27
25
  type t
28
26
  let name: string
29
- let fromJSON: Js.Json.t => result<t, string>
27
+ let fromJSON: JSON.t => result<t, string>
30
28
  let checkFieldsSanity: unit => result<unit, string>
31
29
  }
32
30
 
@@ -39,37 +37,32 @@ module Field = {
39
37
  | Float
40
38
  | Boolean
41
39
  | Array(t)
42
- /// These SHOULD strings in ISO format, but we only validate the string
43
- /// can be represented in Js.Date without spewing NaN all over the place;
44
- /// Js.Date.fromString("xxx") returns an object that is mostly unusable.
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.
45
43
  ///
46
- /// We also allow floats and then use Js.Date.fromFloat.
44
+ // We also allow floats and then use Date.fromTime
47
45
  | Date
48
46
  | Datetime // alias of Date
49
-
50
47
  | Tuple(array<t>)
51
48
  | Object(array<(string, t)>)
52
49
  | Optional(t)
53
50
  | OptionalWithDefault(t, FieldValue.t)
54
- /// An arbitrary mapping from names to other arbitrary fields. The
55
- /// difference with Object, is that you don't know the names of the
56
- /// expected entries.
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.
57
54
  | Mapping(t)
58
-
59
55
  | Deserializer(module(Deserializer))
60
-
61
- /// A specialized Array of deserialized items that ignores unparsable
62
- /// items and returns the valid collection. This saves the user from
63
- /// writing 'Array(DefaultWhenInvalid(Optional(Deserializer(module(M)))))'
64
- /// and then post-process the list of items with 'Array.keepSome'
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'
65
60
  | Collection(module(Deserializer))
66
61
  | DefaultWhenInvalid(t, FieldValue.t)
67
-
68
62
  // FIXME: this is used to add additional restrictions like variadictInt or
69
63
  // variadicString; but I find it too type-unsafe. I might consider having
70
64
  // a Constraints for this in the future.
71
65
  | Morphism(t, FieldValue.t => FieldValue.t)
72
-
73
66
  | Self
74
67
 
75
68
  let usingString = (f: string => 'a) => value => value->FieldValue.asString->f->FieldValue.any
@@ -85,7 +78,7 @@ module Field = {
85
78
  switch i->fromJs {
86
79
  | Some(internalValue) => internalValue
87
80
  | None =>
88
- raise(TypeError(`This Int(${i->Int.toString}) not a valid value here. Hint: ${hint}`))
81
+ throw(TypeError(`This Int(${i->Int.toString}) not a valid value here. Hint: ${hint}`))
89
82
  }
90
83
  }),
91
84
  )
@@ -94,7 +87,7 @@ module Field = {
94
87
  usingString(i => {
95
88
  switch i->fromJs {
96
89
  | Some(internalValue) => internalValue
97
- | None => raise(TypeError(`This String("${i}") not a valid value here. Hint: ${hint}`))
90
+ | None => throw(TypeError(`This String("${i}") not a valid value here. Hint: ${hint}`))
98
91
  }
99
92
  }),
100
93
  )
@@ -135,104 +128,98 @@ module Field = {
135
128
  | DefaultWhenInvalid(t, _) => `Protected ${t->toString}`
136
129
  }
137
130
 
138
- let _taggedToString = (tagged: Js.Json.tagged_t) => {
131
+ let _taggedToString = (tagged: JSON.t) => {
139
132
  switch tagged {
140
- | Js.Json.JSONFalse => "Boolean(false)"
141
- | Js.Json.JSONTrue => "Boolean(true)"
142
- | Js.Json.JSONNull => "Null"
143
- | Js.Json.JSONString(text) => `String("${text}")`
144
- | Js.Json.JSONNumber(number) => `Number(${number->Float.toString})`
145
- | Js.Json.JSONObject(obj) => `Object(${obj->Js.Json.stringifyAny->Option.getOr("...")})`
146
- | Js.Json.JSONArray(array) => `Array(${array->Js.Json.stringifyAny->Option.getOr("...")})`
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("...")})`
147
140
  }
148
141
  }
149
142
 
150
- let rec extractValue = (
151
- values: Dict.t<Js.Json.t>,
152
- field: string,
153
- shape: t,
154
- self: t,
155
- ): FieldValue.t => {
143
+ let rec extractValue = (values: dict<JSON.t>, field: string, shape: t, self: t): FieldValue.t => {
156
144
  switch values->Dict.get(field) {
157
145
  | Some(value) => value->fromUntagged(shape, self)
158
146
  | None =>
159
147
  switch shape {
160
- | DefaultWhenInvalid(_, _) => Js.Json.null->fromUntagged(shape, self)
161
- | Optional(_) => Js.Json.null->fromUntagged(shape, self)
148
+ | DefaultWhenInvalid(_, _) => JSON.Null->fromUntagged(shape, self)
149
+ | Optional(_) => JSON.Null->fromUntagged(shape, self)
162
150
  | OptionalWithDefault(_, default) => default
163
- | _ => raise(TypeError(`Missing non-optional field '${field}'`))
151
+ | _ => throw(TypeError(`Missing non-optional field '${field}'`))
164
152
  }
165
153
  }
166
154
  }
167
- and fromUntagged = (untagged: Js.Json.t, shape: t, self: t): FieldValue.t => {
168
- switch (shape, untagged->Js.Json.classify) {
155
+ and fromUntagged: (JSON.t, t, t) => FieldValue.t = (untagged, shape, self) => {
156
+ switch (shape, untagged) {
169
157
  | (Any, _) => untagged->FieldValue.any
170
- | (Literal(expected), Js.Json.JSONString(text)) =>
171
- if text == expected {
172
- FieldValue.string(text)
173
- } else {
174
- raise(TypeError(`Expecting literal ${expected}, got ${text}`))
175
- }
176
- | (String, Js.Json.JSONString(text)) => FieldValue.string(text)
177
- | (Int, Js.Json.JSONNumber(number)) => FieldValue.int(number->Float.toInt)
178
- | (Float, Js.Json.JSONNumber(number)) => FieldValue.float(number)
179
- | (Boolean, Js.Json.JSONTrue) => FieldValue.boolean(true)
180
- | (Boolean, Js.Json.JSONFalse) => FieldValue.boolean(false)
181
- | (Tuple(bases), Js.Json.JSONArray(items)) => {
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)) => {
182
172
  let lenbases = bases->Array.length
183
173
  let lenitems = items->Array.length
184
174
  if lenbases == lenitems {
185
175
  let values = Belt.Array.zipBy(items, bases, (i, b) => fromUntagged(i, b, self))
186
176
  values->FieldValue.array
187
177
  } else {
188
- raise(
178
+ throw(
189
179
  TypeError(`Expecting ${lenbases->Int.toString} items, got ${lenitems->Int.toString}`),
190
180
  )
191
181
  }
192
182
  }
193
183
 
194
- | (Datetime, Js.Json.JSONString(s))
195
- | (Date, Js.Json.JSONString(s)) => {
196
- let r = Js.Date.fromString(s)
197
- if r->Js.Date.getDate->Js.Float.isNaN {
198
- raise(TypeError(`Invalid date ${s}`))
199
- }
200
- r->FieldValue.any
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}`))
201
188
  }
189
+ r->FieldValue.any
202
190
 
203
- | (Datetime, Js.Json.JSONNumber(f))
204
- | (Date, Js.Json.JSONNumber(f)) => {
205
- let r = Js.Date.fromFloat(f)
206
- if r->Js.Date.getDate->Js.Float.isNaN {
207
- raise(TypeError(`Invalid date ${f->Js.Float.toString}`))
208
- }
209
- r->FieldValue.any
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}`))
210
195
  }
196
+ r->FieldValue.any
211
197
 
212
- | (Array(shape), Js.Json.JSONArray(items)) =>
198
+ | (Array(shape), Array(items)) =>
213
199
  FieldValue.array(items->Array.map(item => item->fromUntagged(shape, self)))
214
- | (Mapping(f), Js.Json.JSONObject(values)) =>
200
+
201
+ | (Mapping(f), Object(values)) =>
215
202
  values->Dict.mapValues(v => v->fromUntagged(f, self))->FieldValue.mapping
216
- | (Object(fields), Js.Json.JSONObject(values)) =>
217
- FieldValue.object(
218
- fields
219
- ->Array.map(((field, shape)) => {
220
- let value = switch extractValue(values, field, shape, self) {
221
- | value => value
222
- | exception TypeError(msg) => raise(TypeError(`Field "${field}": ${msg}`))
223
- }
224
- (field, value)
225
- })
226
- ->Dict.fromArray,
227
- )
228
203
 
229
- | (OptionalWithDefault(_, value), Js.Json.JSONNull) => value
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
230
217
  | (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape, self)
231
- | (Optional(_), Js.Json.JSONNull) => FieldValue.null
218
+ | (Optional(_), Null) => FieldValue.null
232
219
  | (Optional(shape), _) => untagged->fromUntagged(shape, self)
233
220
  | (Morphism(shape, f), _) => untagged->fromUntagged(shape, self)->f->FieldValue.any
234
221
 
235
- | (Collection(m), Js.Json.JSONArray(items)) => {
222
+ | (Collection(m), Array(items)) => {
236
223
  module M = unpack(m: Deserializer)
237
224
  items
238
225
  ->Array.map(M.fromJSON)
@@ -252,7 +239,7 @@ module Field = {
252
239
  module M = unpack(m: Deserializer)
253
240
  switch untagged->M.fromJSON {
254
241
  | Ok(res) => res->FieldValue.any
255
- | Error(msg) => raise(TypeError(msg))
242
+ | Error(msg) => throw(TypeError(msg))
256
243
  }
257
244
  }
258
245
 
@@ -260,13 +247,13 @@ module Field = {
260
247
  switch untagged->fromUntagged(t, self) {
261
248
  | res => res
262
249
  | exception TypeError(msg) => {
263
- Js.Console.warn2("Detected and ignore (with default): ", msg)
250
+ Console.warn2("Detected and ignore (with default): ", msg)
264
251
  default
265
252
  }
266
253
  }
267
254
  | (Self, _) => untagged->fromUntagged(self, self)
268
255
  | (expected, actual) =>
269
- raise(TypeError(`Expected ${expected->toString}, but got ${actual->_taggedToString} instead`))
256
+ throw(TypeError(`Expected ${expected->toString}, but got ${actual->_taggedToString} instead`))
270
257
  }
271
258
  }
272
259
 
@@ -296,21 +283,23 @@ module Field = {
296
283
 
297
284
  | (Object(fields), optional) =>
298
285
  fields
299
- ->Array.map(((fieldName, field)) => () =>
300
- checkFieldsSanity(`${name}::${fieldName}`, field, optional))
286
+ ->Array.map(((fieldName, field)) =>
287
+ () => checkFieldsSanity(`${name}::${fieldName}`, field, optional)
288
+ )
301
289
  ->Array.reduce(Ok([]), (res, nextitem) =>
302
290
  res->Result.flatMap(arr => nextitem()->Result.map(i => arr->Array.concat([i])))
303
291
  )
304
292
  ->Result.map(_ => ())
305
293
 
306
- /// Mappings and arrays can be empty, so their payloads are
307
- /// automatically optional.
294
+ // Mappings and arrays can be empty, so their payloads are
295
+ // automatically optional.
308
296
  | (Mapping(field), _) | (Array(field), _) => checkFieldsSanity(name, field, true)
309
297
 
310
298
  | (Tuple(fields), optional) =>
311
299
  fields
312
- ->Array.mapWithIndex((field, index) => () =>
313
- checkFieldsSanity(`${name}[${index->Int.toString}]`, field, optional))
300
+ ->Array.mapWithIndex((field, index) =>
301
+ () => checkFieldsSanity(`${name}[${index->Int.toString}]`, field, optional)
302
+ )
314
303
  ->Array.reduce(Ok([]), (res, nextitem) =>
315
304
  res->Result.flatMap(arr => nextitem()->Result.map(i => arr->Array.concat([i])))
316
305
  )
@@ -342,8 +331,8 @@ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) =>
342
331
  ")
343
332
  let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
344
333
 
345
- @doc("Parse a `Js.Json.t` into `result<t, string>`")
346
- let fromJSON = (json: Js.Json.t): result<t, _> => {
334
+ @doc("Parse a `'a` into `result<t, string>`")
335
+ let fromJSON: JSON.t => result<t, _> = json => {
347
336
  switch json->Field.fromUntagged(fields, fields) {
348
337
  | res => Ok(res->_toNativeType)
349
338
  | exception TypeError(e) => Error(e)
package/tests/QUnit.res CHANGED
@@ -48,12 +48,12 @@ type done = unit => unit
48
48
 
49
49
  @send external async: assertion => done = "async"
50
50
  @send external asyncMany: (assertion, int) => done = "async"
51
- @send external rejects: (assertion, Js.Promise.t<'a>, message) => unit = "rejects"
52
- @send external rejectsM: (assertion, Js.Promise.t<'a>, message) => unit = "rejects"
51
+ @send external rejects: (assertion, promise<'a>, message) => unit = "rejects"
52
+ @send external rejectsM: (assertion, promise<'a>, message) => unit = "rejects"
53
53
  @send
54
- external rejectMatches: (assertion, Js.Promise.t<'a>, matcher<'a>, message) => unit = "rejects"
54
+ external rejectMatches: (assertion, promise<'a>, matcher<'a>, message) => unit = "rejects"
55
55
  @send
56
- external rejectMatchesM: (assertion, Js.Promise.t<'a>, matcher<'a>, message) => unit = "rejects"
56
+ external rejectMatchesM: (assertion, promise<'a>, matcher<'a>, message) => unit = "rejects"
57
57
  @send external timeout: (assertion, int) => unit = "timeout"
58
58
 
59
59
  // Exceptions
package/tests/index.res CHANGED
@@ -1,10 +1,9 @@
1
1
  open QUnit
2
- open RescriptCore
3
2
 
4
3
  module Appointment = {
5
4
  type t = {
6
5
  note: string,
7
- date: Js.Date.t,
6
+ date: Date.t,
8
7
  extra: option<string>,
9
8
  }
10
9
  module Deserializer = Deser.MakeDeserializer({
@@ -21,7 +20,7 @@ module_("Basic deserializer", _ => {
21
20
  %raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
22
21
  {
23
22
  note: "Bicentennial doctor's appointment",
24
- date: Js.Date.fromString("2100-01-01"),
23
+ date: Date.fromString("2100-01-01"),
25
24
  extra: None,
26
25
  },
27
26
  ),
@@ -33,7 +32,7 @@ module_("Basic deserializer", _ => {
33
32
  }`),
34
33
  {
35
34
  note: "Bicentennial doctor's appointment",
36
- date: Js.Date.fromString("2100-01-01"),
35
+ date: Date.fromString("2100-01-01"),
37
36
  extra: Some("Don't take your stop-aging pills the night before the appointment"),
38
37
  },
39
38
  ),
@@ -43,10 +42,10 @@ module_("Basic deserializer", _ => {
43
42
  qunit->expect(valid->Array.length)
44
43
  valid->Array.forEach(
45
44
  ((data, expected)) => {
46
- Js.Console.log2("Running sample", data)
45
+ Console.log2("Running sample", data)
47
46
  switch Appointment.Deserializer.fromJSON(data) {
48
47
  | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
49
- | Error(msg) => Js.Console.error(msg)
48
+ | Error(msg) => Console.error(msg)
50
49
  }
51
50
  },
52
51
  )
@@ -63,11 +62,11 @@ module_("Basic deserializer", _ => {
63
62
  qunit->expect(invalid->Array.length)
64
63
  invalid->Array.forEach(
65
64
  ((message, data)) => {
66
- Js.Console.log3("Running sample", message, data)
65
+ Console.log3("Running sample", message, data)
67
66
  switch Appointment.Deserializer.fromJSON(data) {
68
- | Ok(result) => Js.Console.error2("Invalid being accepted: ", result)
67
+ | Ok(result) => Console.error2("Invalid being accepted: ", result)
69
68
  | Error(msg) => {
70
- Js.Console.log2("Correctly detected:", msg)
69
+ Console.log2("Correctly detected:", msg)
71
70
  qunit->ok(true, true)
72
71
  }
73
72
  }
@@ -147,10 +146,10 @@ module_("Recursive deserializer", _ => {
147
146
  qunit->expect(valid->Array.length)
148
147
  valid->Array.forEach(
149
148
  ((data, expected)) => {
150
- Js.Console.log2("Running sample", data)
149
+ Console.log2("Running sample", data)
151
150
  switch TrivialTree.Deserializer.fromJSON(data) {
152
151
  | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
153
- | Error(msg) => Js.Console.error(msg)
152
+ | Error(msg) => Console.error(msg)
154
153
  }
155
154
  },
156
155
  )
@@ -187,3 +186,27 @@ module_("Recursive deserializer", _ => {
187
186
  qunit->deepEqual(data->Ledger.fromJSON->Obj.magic, Ok(expected), "nice ledger")
188
187
  })
189
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,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automated test runner for rescript-deser using Playwright
5
+ *
6
+ * This script starts a Vite dev server, runs QUnit tests in a headless browser,
7
+ * and reports the results to the console.
8
+ */
9
+
10
+ const { chromium } = require('@playwright/test');
11
+ const { spawn } = require('child_process');
12
+ const http = require('http');
13
+
14
+ const TEST_PORT = process.env.TEST_PORT || 4370;
15
+ const TEST_URL = `http://localhost:${TEST_PORT}`;
16
+ const STARTUP_TIMEOUT = 30000; // 30 seconds for server startup
17
+ const TEST_TIMEOUT = 120000; // 2 minutes for all tests to complete
18
+
19
+ async function waitForServer(url, timeout) {
20
+ const start = Date.now();
21
+
22
+ while (Date.now() - start < timeout) {
23
+ try {
24
+ await new Promise((resolve, reject) => {
25
+ const req = http.get(url, (res) => {
26
+ resolve();
27
+ });
28
+ req.on('error', reject);
29
+ req.setTimeout(1000);
30
+ });
31
+ return true;
32
+ } catch (err) {
33
+ await new Promise(resolve => setTimeout(resolve, 500));
34
+ }
35
+ }
36
+
37
+ throw new Error(`Server did not start within ${timeout}ms`);
38
+ }
39
+
40
+ async function runTests() {
41
+ let viteProcess = null;
42
+ let browser = null;
43
+ let exitCode = 1;
44
+
45
+ try {
46
+ // Start Vite dev server
47
+ console.log(`Starting Vite server on port ${TEST_PORT}...`);
48
+ viteProcess = spawn('npx', ['vite', '--port', String(TEST_PORT), '--strictPort'], {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ env: { ...process.env }
51
+ });
52
+
53
+ // Capture server output for debugging
54
+ viteProcess.stdout.on('data', (data) => {
55
+ if (process.env.DEBUG) {
56
+ console.log('[vite]', data.toString());
57
+ }
58
+ });
59
+
60
+ viteProcess.stderr.on('data', (data) => {
61
+ if (process.env.DEBUG) {
62
+ console.error('[vite error]', data.toString());
63
+ }
64
+ });
65
+
66
+ // Wait for server to be ready
67
+ console.log('Waiting for server to start...');
68
+ await waitForServer(TEST_URL, STARTUP_TIMEOUT);
69
+ console.log('Server is ready!');
70
+
71
+ // Launch browser
72
+ console.log('Launching browser...');
73
+ browser = await chromium.launch({
74
+ headless: true,
75
+ });
76
+
77
+ const context = await browser.newContext();
78
+ const page = await context.newPage();
79
+
80
+ // Set up console message handling to show test output
81
+ page.on('console', msg => {
82
+ const type = msg.type();
83
+ if (process.env.DEBUG) {
84
+ console.log(`[browser ${type}]`, msg.text());
85
+ }
86
+ });
87
+
88
+ // Navigate to test page
89
+ console.log(`Navigating to ${TEST_URL}...`);
90
+ await page.goto(TEST_URL, { waitUntil: 'networkidle' });
91
+
92
+ // Wait for QUnit to complete and get results
93
+ console.log('Running tests...\n');
94
+ const results = await page.waitForFunction(
95
+ () => {
96
+ if (typeof QUnit !== 'undefined' && QUnit.config && QUnit.config.stats) {
97
+ const done = QUnit.config.current === null || QUnit.config.current === undefined;
98
+ if (done) {
99
+ return {
100
+ passed: QUnit.config.stats.all - QUnit.config.stats.bad,
101
+ failed: QUnit.config.stats.bad,
102
+ total: QUnit.config.stats.all,
103
+ runtime: QUnit.config.stats.runtime
104
+ };
105
+ }
106
+ }
107
+ return false;
108
+ },
109
+ { timeout: TEST_TIMEOUT }
110
+ );
111
+
112
+ const testResults = await results.jsonValue();
113
+
114
+ // Print results
115
+ console.log('========================================');
116
+ console.log(' Test Results');
117
+ console.log('========================================');
118
+ console.log(`Total: ${testResults.total}`);
119
+ console.log(`Passed: ${testResults.passed} ✓`);
120
+ console.log(`Failed: ${testResults.failed} ${testResults.failed > 0 ? '✗' : ''}`);
121
+ console.log(`Runtime: ${testResults.runtime}ms`);
122
+ console.log('========================================');
123
+
124
+ if (testResults.failed > 0) {
125
+ // Get detailed failure information
126
+ const failures = await page.evaluate(() => {
127
+ const failedTests = [];
128
+ const testItems = document.querySelectorAll('#qunit-tests > li.fail');
129
+
130
+ testItems.forEach(item => {
131
+ const testName = item.querySelector('.test-name')?.textContent || 'Unknown test';
132
+ const moduleName = item.querySelector('.module-name')?.textContent || '';
133
+ const assertions = [];
134
+
135
+ item.querySelectorAll('.fail').forEach(assertion => {
136
+ const message = assertion.querySelector('.test-message')?.textContent || '';
137
+ const expected = assertion.querySelector('.test-expected')?.textContent || '';
138
+ const actual = assertion.querySelector('.test-actual')?.textContent || '';
139
+
140
+ assertions.push({ message, expected, actual });
141
+ });
142
+
143
+ failedTests.push({ module: moduleName, test: testName, assertions });
144
+ });
145
+
146
+ return failedTests;
147
+ });
148
+
149
+ console.log('\nFailed Tests:');
150
+ failures.forEach((failure, index) => {
151
+ console.log(`\n${index + 1}. ${failure.module ? failure.module + ' > ' : ''}${failure.test}`);
152
+ failure.assertions.forEach(assertion => {
153
+ if (assertion.message) console.log(` ${assertion.message}`);
154
+ if (assertion.expected) console.log(` Expected: ${assertion.expected}`);
155
+ if (assertion.actual) console.log(` Actual: ${assertion.actual}`);
156
+ });
157
+ });
158
+
159
+ exitCode = 1;
160
+ } else {
161
+ console.log('\n✓ All tests passed!');
162
+ exitCode = 0;
163
+ }
164
+
165
+ } catch (error) {
166
+ console.error('\n✗ Test run failed:');
167
+ console.error(error.message);
168
+ exitCode = 1;
169
+ } finally {
170
+ // Cleanup
171
+ if (browser) {
172
+ await browser.close();
173
+ }
174
+
175
+ if (viteProcess) {
176
+ viteProcess.kill('SIGTERM');
177
+ // Give it a moment to shut down gracefully
178
+ await new Promise(resolve => setTimeout(resolve, 1000));
179
+ if (!viteProcess.killed) {
180
+ viteProcess.kill('SIGKILL');
181
+ }
182
+ }
183
+ }
184
+
185
+ process.exit(exitCode);
186
+ }
187
+
188
+ // Run the tests
189
+ runTests().catch(err => {
190
+ console.error('Unexpected error:', err);
191
+ process.exit(1);
192
+ });