@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.
- package/README.md +16 -5
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/build.ninja +0 -27
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/Deser.ast +0 -0
- package/lib/bs/src/Deser.cmi +0 -0
- package/lib/bs/src/Deser.cmj +0 -0
- package/lib/bs/src/Deser.cmt +0 -0
- package/lib/bs/src/Deser.res +341 -0
- package/lib/bs/tests/QUnit.ast +0 -0
- package/lib/bs/tests/QUnit.cmi +0 -0
- package/lib/bs/tests/QUnit.cmj +0 -0
- package/lib/bs/tests/QUnit.cmt +0 -0
- package/lib/bs/tests/QUnit.res +72 -0
- package/lib/bs/tests/index.ast +0 -0
- package/lib/bs/tests/index.cmi +0 -0
- package/lib/bs/tests/index.cmj +0 -0
- package/lib/bs/tests/index.cmt +0 -0
- package/lib/bs/tests/index.res +212 -0
- package/lib/es6/src/Deser.js +302 -477
- package/lib/es6/tests/index.js +260 -241
- package/lib/js/src/Deser.js +298 -473
- package/lib/js/tests/index.js +261 -242
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/Deser.ast +0 -0
- package/lib/ocaml/Deser.cmi +0 -0
- package/lib/ocaml/Deser.cmj +0 -0
- package/lib/ocaml/Deser.cmt +0 -0
- package/lib/ocaml/Deser.res +341 -0
- package/lib/ocaml/QUnit.ast +0 -0
- package/lib/ocaml/QUnit.cmi +0 -0
- package/lib/ocaml/QUnit.cmj +0 -0
- package/lib/ocaml/QUnit.cmt +0 -0
- package/lib/ocaml/QUnit.res +72 -0
- package/lib/ocaml/index.ast +0 -0
- package/lib/ocaml/index.cmi +0 -0
- package/lib/ocaml/index.cmj +0 -0
- package/lib/ocaml/index.cmt +0 -0
- package/lib/ocaml/index.res +212 -0
- package/lib/rescript.lock +1 -0
- package/package.json +5 -6
- package/rescript.json +4 -6
- package/src/Deser.res +86 -97
- package/tests/QUnit.res +4 -4
- package/tests/index.res +34 -11
- package/tests/run-tests.js +192 -0
- package/yarn.lock +683 -0
- package/lib/bs/.bsbuild +0 -0
- package/lib/bs/.bsdeps +0 -9
- package/lib/bs/.ninja_log +0 -93
- package/lib/bs/.project-files-cache +0 -0
- package/lib/bs/.sourcedirs.json +0 -1
- package/lib/bs/install.ninja +0 -10
- package/lib/bs/src/Deser.d +0 -0
- package/lib/bs/tests/QUnit.d +0 -0
- package/lib/bs/tests/index.d +0 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ The resultant module has type:
|
|
|
50
50
|
module type Deserializer = {
|
|
51
51
|
type t
|
|
52
52
|
let name: string
|
|
53
|
-
let fromJSON:
|
|
53
|
+
let fromJSON: RescriptCore.JSON.t => result<t, string>
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -80,10 +80,10 @@ module type Serializable = {
|
|
|
80
80
|
JS representation of the object that comes from the JSON data.
|
|
81
81
|
|
|
82
82
|
- `Date`, parses either a string representation of a date (datetime) or a
|
|
83
|
-
floating point representation of date (datetime) into `
|
|
83
|
+
floating point representation of date (datetime) into `Date.t`; we make
|
|
84
84
|
sure the result is valid and won't return NaN afterwards.
|
|
85
85
|
|
|
86
|
-
This basically calls, `
|
|
86
|
+
This basically calls, `Date.fromString` or `Date.fromTime`; and tests
|
|
87
87
|
the resulting value.
|
|
88
88
|
|
|
89
89
|
`Datetime` is an alias for `Date`.
|
|
@@ -104,8 +104,7 @@ module type Serializable = {
|
|
|
104
104
|
`Optional`, `OptionalWithDefault`.
|
|
105
105
|
|
|
106
106
|
- `Mapping(Field.t)`, parses a JSON object with unknown keys (of type string)
|
|
107
|
-
and a given type of value. Valid values have the internal type
|
|
108
|
-
`Prelude.Dict.t`.
|
|
107
|
+
and a given type of value. Valid values have the internal type `dict<_>`.
|
|
109
108
|
|
|
110
109
|
- `Deserializer(module(Deserializer))`, parses an JSON object with the function
|
|
111
110
|
`fromJSON` of another deserializer. This allows the composition of
|
|
@@ -136,6 +135,18 @@ type rec Node<'t> = Leave('t) | (Branch(array<Node<'t>>)
|
|
|
136
135
|
Cannot be automatically deserialized.
|
|
137
136
|
|
|
138
137
|
|
|
138
|
+
# Type safety disclaimer
|
|
139
|
+
|
|
140
|
+
`Deser` cannot guarantee type safety for ill-defined deserializers, the following deserializer will accept JSON payloads of the wrong type that can cause runtime type errors:
|
|
141
|
+
|
|
142
|
+
```rescript
|
|
143
|
+
module X = Deser.MakeDeserializer({
|
|
144
|
+
type t = string
|
|
145
|
+
let fields = Deser.Field.Array(Int)
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
|
|
139
150
|
## License
|
|
140
151
|
|
|
141
152
|
The MIT License
|
package/lib/bs/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1766178477130)
|
|
2
|
+
#Done(1766178477131)
|
package/lib/bs/build.ninja
CHANGED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
rescript = 1
|
|
2
|
-
g_finger := /home/manu/src/kaiko/rescript-deser/node_modules/@rescript/core/lib/ocaml/install.stamp
|
|
3
|
-
rule astj
|
|
4
|
-
command = /home/manu/src/kaiko/rescript-deser/node_modules/rescript/linux/bsc.exe -warn-error +8+11+26+33+56 -bs-v 11.1.4 -uncurried -absname -bs-ast -o $out $i
|
|
5
|
-
o tests/index.ast : astj ../../tests/index.res
|
|
6
|
-
rule deps_dev
|
|
7
|
-
command = /home/manu/src/kaiko/rescript-deser/node_modules/rescript/linux/bsb_helper.exe -g -hash 9a7a4be86983dcab705bb67a1ec483c7 $in
|
|
8
|
-
restat = 1
|
|
9
|
-
o tests/index.d : deps_dev tests/index.ast
|
|
10
|
-
rule mij_dev
|
|
11
|
-
command = /home/manu/src/kaiko/rescript-deser/node_modules/rescript/linux/bsc.exe -I tests -I src/ -I /home/manu/src/kaiko/rescript-deser/node_modules/@rescript/core/lib/ocaml -warn-error +8+11+26+33+56 -uncurried -bs-package-name @kaiko.io/rescript-deser -bs-package-output commonjs:lib/js/$in_d:.js -bs-package-output esmodule:lib/es6/$in_d:.js -bs-v $g_finger $i
|
|
12
|
-
dyndep = 1
|
|
13
|
-
restat = 1
|
|
14
|
-
o tests/index.cmj tests/index.cmi ../es6/tests/index.js ../js/tests/index.js : mij_dev tests/index.ast
|
|
15
|
-
o tests/QUnit.ast : astj ../../tests/QUnit.res
|
|
16
|
-
o tests/QUnit.d : deps_dev tests/QUnit.ast
|
|
17
|
-
o tests/QUnit.cmj tests/QUnit.cmi ../es6/tests/QUnit.js ../js/tests/QUnit.js : mij_dev tests/QUnit.ast
|
|
18
|
-
o src/Deser.ast : astj ../../src/Deser.res
|
|
19
|
-
rule deps
|
|
20
|
-
command = /home/manu/src/kaiko/rescript-deser/node_modules/rescript/linux/bsb_helper.exe -hash 9a7a4be86983dcab705bb67a1ec483c7 $in
|
|
21
|
-
restat = 1
|
|
22
|
-
o src/Deser.d : deps src/Deser.ast
|
|
23
|
-
rule mij
|
|
24
|
-
command = /home/manu/src/kaiko/rescript-deser/node_modules/rescript/linux/bsc.exe -I src/ -I /home/manu/src/kaiko/rescript-deser/node_modules/@rescript/core/lib/ocaml -warn-error +8+11+26+33+56 -uncurried -bs-package-name @kaiko.io/rescript-deser -bs-package-output commonjs:lib/js/$in_d:.js -bs-package-output esmodule:lib/es6/$in_d:.js -bs-v $g_finger $i
|
|
25
|
-
dyndep = 1
|
|
26
|
-
restat = 1
|
|
27
|
-
o src/Deser.cmj src/Deser.cmi ../es6/src/Deser.js ../js/src/Deser.js : mij src/Deser.ast
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "12.0.1",
|
|
3
|
+
"bsc_path": "/home/manu/src/kaiko/rescript-deser/node_modules/@rescript/linux-x64/bin/bsc.exe",
|
|
4
|
+
"bsc_hash": "a2b93197b8c05fc70981fe131a9ed75f8462a9f615f71055f306c9deb058d3cf",
|
|
5
|
+
"rescript_config_hash": "6e13fe97e162573c9ab73327567294583b34d549775f8391e1d5c8ccc0c5d580",
|
|
6
|
+
"runtime_path": "/home/manu/src/kaiko/rescript-deser/node_modules/@rescript/runtime",
|
|
7
|
+
"generated_at": "1766178477132"
|
|
8
|
+
}
|
package/lib/bs/src/Deser.ast
CHANGED
|
Binary file
|
package/lib/bs/src/Deser.cmi
CHANGED
|
Binary file
|
package/lib/bs/src/Deser.cmj
CHANGED
|
Binary file
|
package/lib/bs/src/Deser.cmt
CHANGED
|
Binary file
|
|
@@ -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
|
+
}
|
package/lib/bs/tests/QUnit.ast
CHANGED
|
Binary file
|
package/lib/bs/tests/QUnit.cmi
CHANGED
|
Binary file
|
package/lib/bs/tests/QUnit.cmj
CHANGED
|
Binary file
|
package/lib/bs/tests/QUnit.cmt
CHANGED
|
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"
|
package/lib/bs/tests/index.ast
CHANGED
|
Binary file
|
package/lib/bs/tests/index.cmi
CHANGED
|
Binary file
|
package/lib/bs/tests/index.cmj
CHANGED
|
Binary file
|
package/lib/bs/tests/index.cmt
CHANGED
|
Binary file
|