@kaiko.io/rescript-deser 6.0.0 → 7.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/src/Deser.res CHANGED
@@ -26,7 +26,7 @@ exception TypeError(string)
26
26
  module type Deserializer = {
27
27
  type t
28
28
  let name: string
29
- let fromJSON: Js.Json.t => result<t, string>
29
+ let fromJSON: JSON.t => result<t, string>
30
30
  let checkFieldsSanity: unit => result<unit, string>
31
31
  }
32
32
 
@@ -39,11 +39,11 @@ module Field = {
39
39
  | Float
40
40
  | Boolean
41
41
  | 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.
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.
45
45
  ///
46
- /// We also allow floats and then use Js.Date.fromFloat.
46
+ // We also allow floats and then use Date.fromTime
47
47
  | Date
48
48
  | Datetime // alias of Date
49
49
 
@@ -51,17 +51,17 @@ module Field = {
51
51
  | Object(array<(string, t)>)
52
52
  | Optional(t)
53
53
  | 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.
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.
57
57
  | Mapping(t)
58
58
 
59
59
  | Deserializer(module(Deserializer))
60
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'
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'
65
65
  | Collection(module(Deserializer))
66
66
  | DefaultWhenInvalid(t, FieldValue.t)
67
67
 
@@ -135,20 +135,20 @@ module Field = {
135
135
  | DefaultWhenInvalid(t, _) => `Protected ${t->toString}`
136
136
  }
137
137
 
138
- let _taggedToString = (tagged: Js.Json.tagged_t) => {
138
+ let _taggedToString = (tagged: JSON.t) => {
139
139
  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("...")})`
140
+ | Boolean(false) => "Boolean(false)"
141
+ | Boolean(true) => "Boolean(true)"
142
+ | Null => "Null"
143
+ | String(text) => `String("${text}")`
144
+ | Number(number) => `Number(${number->Float.toString})`
145
+ | Object(obj) => `Object(${obj->JSON.stringifyAny->Option.getOr("...")})`
146
+ | Array(array) => `Array(${array->JSON.stringifyAny->Option.getOr("...")})`
147
147
  }
148
148
  }
149
149
 
150
150
  let rec extractValue = (
151
- values: Dict.t<Js.Json.t>,
151
+ values: Dict.t<JSON.t>,
152
152
  field: string,
153
153
  shape: t,
154
154
  self: t,
@@ -157,28 +157,30 @@ module Field = {
157
157
  | Some(value) => value->fromUntagged(shape, self)
158
158
  | None =>
159
159
  switch shape {
160
- | DefaultWhenInvalid(_, _) => Js.Json.null->fromUntagged(shape, self)
161
- | Optional(_) => Js.Json.null->fromUntagged(shape, self)
160
+ | DefaultWhenInvalid(_, _) => JSON.Null->fromUntagged(shape, self)
161
+ | Optional(_) => JSON.Null->fromUntagged(shape, self)
162
162
  | OptionalWithDefault(_, default) => default
163
163
  | _ => raise(TypeError(`Missing non-optional field '${field}'`))
164
164
  }
165
165
  }
166
166
  }
167
- and fromUntagged = (untagged: Js.Json.t, shape: t, self: t): FieldValue.t => {
168
- switch (shape, untagged->Js.Json.classify) {
167
+ and fromUntagged: (JSON.t, t, t) => FieldValue.t = (untagged, shape, self) => {
168
+ switch (shape, untagged) {
169
169
  | (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)) => {
170
+
171
+ | (Literal(expected), String(text)) if text == expected => FieldValue.string(text)
172
+ | (Literal(expected), String(text)) =>
173
+ raise(TypeError(`Expecting literal ${expected}, got ${text}`))
174
+
175
+ | (String, String(text)) => FieldValue.string(text)
176
+
177
+ | (Int, Number(number)) => FieldValue.int(number->Float.toInt)
178
+
179
+ | (Float, Number(number)) => FieldValue.float(number)
180
+
181
+ | (Boolean, Boolean(v)) => FieldValue.boolean(v)
182
+
183
+ | (Tuple(bases), Array(items)) => {
182
184
  let lenbases = bases->Array.length
183
185
  let lenitems = items->Array.length
184
186
  if lenbases == lenitems {
@@ -191,52 +193,56 @@ module Field = {
191
193
  }
192
194
  }
193
195
 
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
196
+ | (Datetime | Date, String(s)) =>
197
+ let r = Date.fromString(s)
198
+ if r->Date.getDate->Int.toFloat->Float.isNaN {
199
+ raise(TypeError(`Invalid date ${s}`))
201
200
  }
201
+ r->FieldValue.any
202
202
 
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
203
+ | (Datetime | Date, Number(f)) =>
204
+ let r = Date.fromTime(f)
205
+ if r->Date.getDate->Int.toFloat->Float.isNaN {
206
+ raise(TypeError(`Invalid date ${f->Float.toString}`))
210
207
  }
208
+ r->FieldValue.any
211
209
 
212
- | (Array(shape), Js.Json.JSONArray(items)) =>
210
+ | (Array(shape), Array(items)) =>
213
211
  FieldValue.array(items->Array.map(item => item->fromUntagged(shape, self)))
214
- | (Mapping(f), Js.Json.JSONObject(values)) =>
212
+
213
+ | (Mapping(f), Object(values)) =>
215
214
  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
215
 
229
- | (OptionalWithDefault(_, value), Js.Json.JSONNull) => value
216
+ | (Object(fields), Object(values)) =>
217
+ fields
218
+ ->Array.map(((field, shape)) => {
219
+ let value = switch extractValue(values, field, shape, self) {
220
+ | value => value
221
+ | exception TypeError(msg) => raise(TypeError(`Field "${field}": ${msg}`))
222
+ }
223
+ (field, value)
224
+ })
225
+ ->Dict.fromArray
226
+ ->FieldValue.object
227
+
228
+ | (OptionalWithDefault(_, value), Null) => value
230
229
  | (OptionalWithDefault(shape, _), _) => untagged->fromUntagged(shape, self)
231
- | (Optional(_), Js.Json.JSONNull) => FieldValue.null
230
+ | (Optional(_), Null) => FieldValue.null
232
231
  | (Optional(shape), _) => untagged->fromUntagged(shape, self)
233
232
  | (Morphism(shape, f), _) => untagged->fromUntagged(shape, self)->f->FieldValue.any
234
233
 
235
- | (Collection(m), Js.Json.JSONArray(items)) => {
234
+ | (Collection(m), Array(items)) => {
236
235
  module M = unpack(m: Deserializer)
237
236
  items
238
237
  ->Array.map(M.fromJSON)
239
- ->Array.filter(x => x->Result.isOk)
238
+ ->Array.filterMap(x =>
239
+ switch x {
240
+ | Ok(v) => Some(v)
241
+ | Error(msg) =>
242
+ Console.warn3(__MODULE__, "Could not deserialize value in the collection", msg)
243
+ None
244
+ }
245
+ )
240
246
  ->Array.map(FieldValue.any)
241
247
  ->FieldValue.array
242
248
  }
@@ -253,7 +259,7 @@ module Field = {
253
259
  switch untagged->fromUntagged(t, self) {
254
260
  | res => res
255
261
  | exception TypeError(msg) => {
256
- Js.Console.warn2("Detected and ignore (with default): ", msg)
262
+ Console.warn2("Detected and ignore (with default): ", msg)
257
263
  default
258
264
  }
259
265
  }
@@ -296,8 +302,8 @@ module Field = {
296
302
  )
297
303
  ->Result.map(_ => ())
298
304
 
299
- /// Mappings and arrays can be empty, so their payloads are
300
- /// automatically optional.
305
+ // Mappings and arrays can be empty, so their payloads are
306
+ // automatically optional.
301
307
  | (Mapping(field), _) | (Array(field), _) => checkFieldsSanity(name, field, true)
302
308
 
303
309
  | (Tuple(fields), optional) =>
@@ -335,8 +341,8 @@ module MakeDeserializer = (S: Serializable): (Deserializer with type t = S.t) =>
335
341
  ")
336
342
  let checkFieldsSanity = () => Field.checkFieldsSanity(name, fields, false)
337
343
 
338
- @doc("Parse a `Js.Json.t` into `result<t, string>`")
339
- let fromJSON = (json: Js.Json.t): result<t, _> => {
344
+ @doc("Parse a `'a` into `result<t, string>`")
345
+ let fromJSON: JSON.t => result<t, _> = json => {
340
346
  switch json->Field.fromUntagged(fields, fields) {
341
347
  | res => Ok(res->_toNativeType)
342
348
  | exception TypeError(e) => Error(e)
package/tests/index.res CHANGED
@@ -4,7 +4,7 @@ open RescriptCore
4
4
  module Appointment = {
5
5
  type t = {
6
6
  note: string,
7
- date: Js.Date.t,
7
+ date: Date.t,
8
8
  extra: option<string>,
9
9
  }
10
10
  module Deserializer = Deser.MakeDeserializer({
@@ -21,7 +21,7 @@ module_("Basic deserializer", _ => {
21
21
  %raw(`{"note": "Bicentennial doctor's appointment", "date": "2100-01-01"}`),
22
22
  {
23
23
  note: "Bicentennial doctor's appointment",
24
- date: Js.Date.fromString("2100-01-01"),
24
+ date: Date.fromString("2100-01-01"),
25
25
  extra: None,
26
26
  },
27
27
  ),
@@ -33,7 +33,7 @@ module_("Basic deserializer", _ => {
33
33
  }`),
34
34
  {
35
35
  note: "Bicentennial doctor's appointment",
36
- date: Js.Date.fromString("2100-01-01"),
36
+ date: Date.fromString("2100-01-01"),
37
37
  extra: Some("Don't take your stop-aging pills the night before the appointment"),
38
38
  },
39
39
  ),
@@ -43,10 +43,10 @@ module_("Basic deserializer", _ => {
43
43
  qunit->expect(valid->Array.length)
44
44
  valid->Array.forEach(
45
45
  ((data, expected)) => {
46
- Js.Console.log2("Running sample", data)
46
+ Console.log2("Running sample", data)
47
47
  switch Appointment.Deserializer.fromJSON(data) {
48
48
  | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
49
- | Error(msg) => Js.Console.error(msg)
49
+ | Error(msg) => Console.error(msg)
50
50
  }
51
51
  },
52
52
  )
@@ -63,11 +63,11 @@ module_("Basic deserializer", _ => {
63
63
  qunit->expect(invalid->Array.length)
64
64
  invalid->Array.forEach(
65
65
  ((message, data)) => {
66
- Js.Console.log3("Running sample", message, data)
66
+ Console.log3("Running sample", message, data)
67
67
  switch Appointment.Deserializer.fromJSON(data) {
68
- | Ok(result) => Js.Console.error2("Invalid being accepted: ", result)
68
+ | Ok(result) => Console.error2("Invalid being accepted: ", result)
69
69
  | Error(msg) => {
70
- Js.Console.log2("Correctly detected:", msg)
70
+ Console.log2("Correctly detected:", msg)
71
71
  qunit->ok(true, true)
72
72
  }
73
73
  }
@@ -147,10 +147,10 @@ module_("Recursive deserializer", _ => {
147
147
  qunit->expect(valid->Array.length)
148
148
  valid->Array.forEach(
149
149
  ((data, expected)) => {
150
- Js.Console.log2("Running sample", data)
150
+ Console.log2("Running sample", data)
151
151
  switch TrivialTree.Deserializer.fromJSON(data) {
152
152
  | Ok(result) => qunit->deepEqual(result, expected, "result == expected")
153
- | Error(msg) => Js.Console.error(msg)
153
+ | Error(msg) => Console.error(msg)
154
154
  }
155
155
  },
156
156
  )
@@ -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
+ });