@mikrojs/native 0.18.2 → 0.19.0-pr-352.20260829215534
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/CMakeLists.txt +25 -2
- package/include/mikrojs/mem.h +4 -0
- package/include/mikrojs/ota_client.h +3 -3
- package/include/mikrojs/platform.h +8 -0
- package/include/mikrojs/private.h +28 -0
- package/package.json +4 -5
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/http/helpers.ts +5 -0
- package/runtime/internal.d.ts +41 -2
- package/runtime/ota/client.ts +4 -0
- package/runtime/ota/ota.ts +4 -0
- package/runtime/ota/types.ts +75 -0
- package/runtime/schema/schema.ts +25 -3
- package/runtime/schema/types.ts +193 -15
- package/runtime/sys/types.ts +11 -1
- package/runtime/test/test.ts +6 -2
- package/runtime/wifi/types.ts +3 -0
- package/scripts/gen-schema-fixtures.js +317 -0
- package/src/mem.cpp +83 -37
- package/src/mik_app_config.cpp +9 -1
- package/src/mik_http_client.cpp +1357 -0
- package/src/mik_ota_client.cpp +3 -3
- package/src/mik_repl.cpp +44 -5
- package/src/mik_schema.cpp +892 -0
- package/src/mik_sys.cpp +9 -1
- package/src/mik_wifi_client.cpp +742 -0
- package/src/mikrojs.cpp +34 -8
- package/src/modules.cpp +66 -0
- package/dist/runtime/schema/core.d.ts +0 -115
- package/dist/runtime/schema/core.d.ts.map +0 -1
- package/dist/runtime/schema/core.js +0 -259
- package/dist/runtime/schema/core.js.map +0 -1
- package/dist/runtime/schema/shared.d.ts +0 -54
- package/dist/runtime/schema/shared.d.ts.map +0 -1
- package/dist/runtime/schema/shared.js +0 -489
- package/dist/runtime/schema/shared.js.map +0 -1
- package/runtime/http/native.ts +0 -160
- package/runtime/http/request.ts +0 -25
- package/runtime/schema/core.ts +0 -539
- package/runtime/schema/shared.ts +0 -494
- package/runtime/wifi/wifi.ts +0 -191
package/runtime/schema/types.ts
CHANGED
|
@@ -2,17 +2,153 @@ import type {Result} from '../result/types.js'
|
|
|
2
2
|
|
|
3
3
|
type Primitive = string | number | boolean
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export type Format = 'url' | 'hostname' | 'ipv4' | 'mac' | 'email'
|
|
6
|
+
|
|
7
|
+
export type Unit =
|
|
8
|
+
| 'm'
|
|
9
|
+
| 'kg'
|
|
10
|
+
| 's'
|
|
11
|
+
| 'A'
|
|
12
|
+
| 'K'
|
|
13
|
+
| 'cd'
|
|
14
|
+
| 'mol'
|
|
15
|
+
| 'Hz'
|
|
16
|
+
| 'rad'
|
|
17
|
+
| 'sr'
|
|
18
|
+
| 'N'
|
|
19
|
+
| 'Pa'
|
|
20
|
+
| 'J'
|
|
21
|
+
| 'W'
|
|
22
|
+
| 'C'
|
|
23
|
+
| 'V'
|
|
24
|
+
| 'F'
|
|
25
|
+
| 'Ohm'
|
|
26
|
+
| 'S'
|
|
27
|
+
| 'Wb'
|
|
28
|
+
| 'T'
|
|
29
|
+
| 'H'
|
|
30
|
+
| 'Cel'
|
|
31
|
+
| 'lm'
|
|
32
|
+
| 'lx'
|
|
33
|
+
| 'Bq'
|
|
34
|
+
| 'Gy'
|
|
35
|
+
| 'Sv'
|
|
36
|
+
| 'kat'
|
|
37
|
+
| 'm2'
|
|
38
|
+
| 'm3'
|
|
39
|
+
| 'm/s'
|
|
40
|
+
| 'm/s2'
|
|
41
|
+
| 'm3/s'
|
|
42
|
+
| 'W/m2'
|
|
43
|
+
| 'cd/m2'
|
|
44
|
+
| 'bit'
|
|
45
|
+
| 'bit/s'
|
|
46
|
+
| 'lat'
|
|
47
|
+
| 'lon'
|
|
48
|
+
| 'pH'
|
|
49
|
+
| 'dB'
|
|
50
|
+
| 'dBW'
|
|
51
|
+
| 'count'
|
|
52
|
+
| '/'
|
|
53
|
+
| '%RH'
|
|
54
|
+
| '%EL'
|
|
55
|
+
| 'EL'
|
|
56
|
+
| '1/s'
|
|
57
|
+
| 'S/m'
|
|
58
|
+
| 'B'
|
|
59
|
+
| 'VA'
|
|
60
|
+
| 'VAs'
|
|
61
|
+
| 'var'
|
|
62
|
+
| 'vars'
|
|
63
|
+
| 'J/m'
|
|
64
|
+
| 'kg/m3'
|
|
65
|
+
| 'deg'
|
|
66
|
+
| 'NTU'
|
|
67
|
+
| 'ms'
|
|
68
|
+
| 'min'
|
|
69
|
+
| 'h'
|
|
70
|
+
| 'MHz'
|
|
71
|
+
| 'kW'
|
|
72
|
+
| 'kVA'
|
|
73
|
+
| 'kvar'
|
|
74
|
+
| 'Ah'
|
|
75
|
+
| 'Wh'
|
|
76
|
+
| 'kWh'
|
|
77
|
+
| 'varh'
|
|
78
|
+
| 'kvarh'
|
|
79
|
+
| 'kVAh'
|
|
80
|
+
| 'Wh/km'
|
|
81
|
+
| 'KiB'
|
|
82
|
+
| 'GB'
|
|
83
|
+
| 'Mbit/s'
|
|
84
|
+
| 'B/s'
|
|
85
|
+
| 'MB/s'
|
|
86
|
+
| 'mV'
|
|
87
|
+
| 'mA'
|
|
88
|
+
| 'dBm'
|
|
89
|
+
| 'ug/m3'
|
|
90
|
+
| 'mm/h'
|
|
91
|
+
| 'm/h'
|
|
92
|
+
| 'ppm'
|
|
93
|
+
| '/100'
|
|
94
|
+
| '/1000'
|
|
95
|
+
| 'hPa'
|
|
96
|
+
| 'mm'
|
|
97
|
+
| 'cm'
|
|
98
|
+
| 'km'
|
|
99
|
+
| 'km/h'
|
|
100
|
+
| 'ppb'
|
|
101
|
+
| 'ppt'
|
|
102
|
+
| 'VAh'
|
|
103
|
+
| 'mg/l'
|
|
104
|
+
| 'ug/l'
|
|
105
|
+
| 'g/l'
|
|
106
|
+
| 'us'
|
|
107
|
+
| 'kHz'
|
|
108
|
+
| 'GHz'
|
|
109
|
+
| 'mW'
|
|
110
|
+
| 'uA'
|
|
111
|
+
| 'uV'
|
|
112
|
+
| 'mAh'
|
|
113
|
+
| 'MiB'
|
|
114
|
+
| 'kB'
|
|
115
|
+
| 'MB'
|
|
116
|
+
| 'kbit/s'
|
|
117
|
+
| 'KiB/s'
|
|
118
|
+
| 'kohm'
|
|
119
|
+
| 'Mohm'
|
|
120
|
+
| 'kPa'
|
|
121
|
+
| 'bar'
|
|
122
|
+
| 'Bd'
|
|
123
|
+
|
|
124
|
+
/* Display annotations, carried by every node a form can render. They never
|
|
125
|
+
* change what validates. Not on optional(): the wrapper expresses absence, the
|
|
126
|
+
* node it wraps expresses identity, so annotations go on the inner. */
|
|
127
|
+
interface Annotated {
|
|
128
|
+
readonly title?: string
|
|
129
|
+
readonly description?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface StringSchema extends Annotated {
|
|
6
133
|
readonly kind: 'string'
|
|
7
134
|
readonly default?: string
|
|
135
|
+
readonly mask?: boolean
|
|
136
|
+
readonly minLength?: number
|
|
137
|
+
readonly maxLength?: number
|
|
138
|
+
readonly format?: Format
|
|
8
139
|
}
|
|
9
140
|
|
|
10
|
-
export interface NumberSchema {
|
|
141
|
+
export interface NumberSchema extends Annotated {
|
|
11
142
|
readonly kind: 'number'
|
|
12
143
|
readonly default?: number
|
|
144
|
+
readonly mask?: boolean
|
|
145
|
+
readonly min?: number
|
|
146
|
+
readonly max?: number
|
|
147
|
+
readonly integer?: boolean
|
|
148
|
+
readonly unit?: Unit
|
|
13
149
|
}
|
|
14
150
|
|
|
15
|
-
export interface BooleanSchema {
|
|
151
|
+
export interface BooleanSchema extends Annotated {
|
|
16
152
|
readonly kind: 'boolean'
|
|
17
153
|
readonly default?: boolean
|
|
18
154
|
}
|
|
@@ -21,19 +157,23 @@ export interface UnknownSchema {
|
|
|
21
157
|
readonly kind: 'unknown'
|
|
22
158
|
}
|
|
23
159
|
|
|
24
|
-
export interface LiteralSchema<T extends Primitive = Primitive> {
|
|
160
|
+
export interface LiteralSchema<T extends Primitive = Primitive> extends Annotated {
|
|
25
161
|
readonly kind: 'literal'
|
|
26
162
|
readonly value: T
|
|
27
163
|
readonly default?: T
|
|
28
164
|
}
|
|
29
165
|
|
|
30
|
-
export interface ArraySchema<S extends Schema = Schema> {
|
|
166
|
+
export interface ArraySchema<S extends Schema = Schema> extends Annotated {
|
|
31
167
|
readonly kind: 'array'
|
|
32
168
|
readonly element: S
|
|
33
169
|
readonly default?: unknown
|
|
170
|
+
readonly minItems?: number
|
|
171
|
+
readonly maxItems?: number
|
|
34
172
|
}
|
|
35
173
|
|
|
36
|
-
export interface ObjectSchema<
|
|
174
|
+
export interface ObjectSchema<
|
|
175
|
+
Shape extends Record<string, Schema> = Record<string, Schema>,
|
|
176
|
+
> extends Annotated {
|
|
37
177
|
readonly kind: 'object'
|
|
38
178
|
readonly shape: Shape
|
|
39
179
|
}
|
|
@@ -43,13 +183,17 @@ export interface OptionalSchema<S extends Schema = Schema> {
|
|
|
43
183
|
readonly inner: S
|
|
44
184
|
}
|
|
45
185
|
|
|
46
|
-
export interface TupleSchema<
|
|
186
|
+
export interface TupleSchema<
|
|
187
|
+
Elements extends readonly Schema[] = readonly Schema[],
|
|
188
|
+
> extends Annotated {
|
|
47
189
|
readonly kind: 'tuple'
|
|
48
190
|
readonly elements: Elements
|
|
49
191
|
readonly default?: unknown
|
|
50
192
|
}
|
|
51
193
|
|
|
52
|
-
export interface UnionSchema<
|
|
194
|
+
export interface UnionSchema<
|
|
195
|
+
Members extends readonly Schema[] = readonly Schema[],
|
|
196
|
+
> extends Annotated {
|
|
53
197
|
readonly kind: 'union'
|
|
54
198
|
readonly members: Members
|
|
55
199
|
readonly default?: unknown
|
|
@@ -58,7 +202,7 @@ export interface UnionSchema<Members extends readonly Schema[] = readonly Schema
|
|
|
58
202
|
export interface TaggedUnionSchema<
|
|
59
203
|
Key extends string = string,
|
|
60
204
|
Branches extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,
|
|
61
|
-
> {
|
|
205
|
+
> extends Annotated {
|
|
62
206
|
readonly kind: 'taggedUnion'
|
|
63
207
|
readonly key: Key
|
|
64
208
|
readonly branches: Branches
|
|
@@ -137,7 +281,7 @@ export type InferRead<S> =
|
|
|
137
281
|
: Simplify<InferReadObject<Shape>>
|
|
138
282
|
: Infer<S>
|
|
139
283
|
|
|
140
|
-
/* Kept in lockstep with
|
|
284
|
+
/* Kept in lockstep with @mikrojs/schema (and its materializeDefaults). */
|
|
141
285
|
type Filled<S> = S extends {default: unknown}
|
|
142
286
|
? true
|
|
143
287
|
: S extends OptionalSchema
|
|
@@ -162,22 +306,52 @@ type InferReadObject<Shape> = {
|
|
|
162
306
|
|
|
163
307
|
export type SchemaError = {name: 'ValidationFailed'; message: string; path: string}
|
|
164
308
|
|
|
165
|
-
export interface
|
|
309
|
+
export interface DisplayOptions {
|
|
310
|
+
readonly title?: string
|
|
311
|
+
readonly description?: string
|
|
312
|
+
}
|
|
313
|
+
export interface ScalarOptions<T> extends DisplayOptions {
|
|
166
314
|
readonly default?: T
|
|
167
315
|
}
|
|
168
|
-
export interface DefaultOption<T> {
|
|
316
|
+
export interface DefaultOption<T> extends DisplayOptions {
|
|
169
317
|
readonly default?: T
|
|
170
318
|
}
|
|
319
|
+
export interface MaskableOptions<T> extends ScalarOptions<T> {
|
|
320
|
+
readonly mask?: boolean
|
|
321
|
+
}
|
|
322
|
+
export interface StringOptions<T> extends MaskableOptions<T> {
|
|
323
|
+
readonly minLength?: number
|
|
324
|
+
readonly maxLength?: number
|
|
325
|
+
readonly format?: Format
|
|
326
|
+
}
|
|
327
|
+
export interface NumberOptions<T> extends MaskableOptions<T> {
|
|
328
|
+
readonly min?: number
|
|
329
|
+
readonly max?: number
|
|
330
|
+
readonly integer?: boolean
|
|
331
|
+
readonly unit?: Unit
|
|
332
|
+
}
|
|
333
|
+
export interface ArrayOptions<T> extends DefaultOption<T> {
|
|
334
|
+
readonly minItems?: number
|
|
335
|
+
readonly maxItems?: number
|
|
336
|
+
}
|
|
337
|
+
export interface EnumEntry<T extends Primitive> {
|
|
338
|
+
readonly value: T
|
|
339
|
+
readonly title?: string
|
|
340
|
+
readonly description?: string
|
|
341
|
+
}
|
|
342
|
+
type EnumMembers<Entries extends readonly EnumEntry<Primitive>[]> = {
|
|
343
|
+
[K in keyof Entries]: LiteralSchema<Entries[K]['value']>
|
|
344
|
+
}
|
|
171
345
|
|
|
172
346
|
/* Mirrors core.ts: the constructors record a `default` annotation in their
|
|
173
347
|
* return type, which is what lets InferRead see it. */
|
|
174
348
|
type Defaulted<S, D> = [D] extends [undefined] ? S : S & {readonly default: unknown}
|
|
175
349
|
|
|
176
350
|
export declare function string<D extends string | undefined = undefined>(
|
|
177
|
-
options?:
|
|
351
|
+
options?: StringOptions<D>,
|
|
178
352
|
): Defaulted<StringSchema, D>
|
|
179
353
|
export declare function number<D extends number | undefined = undefined>(
|
|
180
|
-
options?:
|
|
354
|
+
options?: NumberOptions<D>,
|
|
181
355
|
): Defaulted<NumberSchema, D>
|
|
182
356
|
export declare function boolean<D extends boolean | undefined = undefined>(
|
|
183
357
|
options?: ScalarOptions<D>,
|
|
@@ -190,7 +364,7 @@ export declare function literal<T extends Primitive, D extends T | undefined = u
|
|
|
190
364
|
export declare function array<
|
|
191
365
|
S extends Schema,
|
|
192
366
|
D extends NoInfer<Infer<S>>[] | undefined = undefined,
|
|
193
|
-
>(element: S, options?:
|
|
367
|
+
>(element: S, options?: ArrayOptions<D>): Defaulted<ArraySchema<S>, D>
|
|
194
368
|
export declare function object<Shape extends Record<string, Schema>>(
|
|
195
369
|
shape: Shape,
|
|
196
370
|
options?: DefaultOption<never>,
|
|
@@ -204,6 +378,10 @@ export declare function union<
|
|
|
204
378
|
Members extends readonly Schema[],
|
|
205
379
|
D extends NoInfer<Infer<UnionSchema<Members>>> | undefined = undefined,
|
|
206
380
|
>(members: [...Members], options?: DefaultOption<D>): Defaulted<UnionSchema<Members>, D>
|
|
381
|
+
export declare function enumOf<
|
|
382
|
+
const Entries extends readonly EnumEntry<Primitive>[],
|
|
383
|
+
D extends Entries[number]['value'] | undefined = undefined,
|
|
384
|
+
>(entries: Entries, options?: DefaultOption<D>): Defaulted<UnionSchema<EnumMembers<Entries>>, D>
|
|
207
385
|
export declare function taggedUnion<
|
|
208
386
|
Key extends string,
|
|
209
387
|
Branches extends Record<string, ObjectSchema>,
|
package/runtime/sys/types.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type {Result} from '../result/types.js'
|
|
2
2
|
|
|
3
3
|
export interface MemoryUsage {
|
|
4
|
+
/** Bytes allocated from the JS heap. On device this counts real
|
|
5
|
+
* allocator block sizes (requested bytes rounded up to the heap's
|
|
6
|
+
* granularity), so it reflects what the heap actually spends; small
|
|
7
|
+
* run-to-run variation is normal. */
|
|
4
8
|
heapUsed: number
|
|
9
|
+
/** The JS heap's configured memory limit in bytes. */
|
|
5
10
|
heapTotal: number
|
|
6
11
|
/** Free system heap in bytes (0 on host) */
|
|
7
12
|
systemFree: number
|
|
@@ -87,6 +92,11 @@ export declare function gc(): void
|
|
|
87
92
|
/** Firmware version string (e.g. "0.1.0") */
|
|
88
93
|
export declare const version: string
|
|
89
94
|
|
|
95
|
+
/** Values {@link BoardInfo.features} can contain. `wifi`, `ble` and `bt` need
|
|
96
|
+
* both the silicon and the matching stack compiled into the firmware;
|
|
97
|
+
* `ieee802154` reports silicon alone. */
|
|
98
|
+
export type BoardFeature = 'wifi' | 'ble' | 'bt' | 'ieee802154'
|
|
99
|
+
|
|
90
100
|
export interface BoardInfo {
|
|
91
101
|
/** Board name from the firmware build (e.g. "xiao-esp32c6") or "generic" */
|
|
92
102
|
name: string
|
|
@@ -97,7 +107,7 @@ export interface BoardInfo {
|
|
|
97
107
|
/** Silicon revision (major * 100 + minor on ESP32, 0 on host) */
|
|
98
108
|
revision: number
|
|
99
109
|
/** Supported features (e.g. ["wifi", "ble"]) */
|
|
100
|
-
features:
|
|
110
|
+
features: BoardFeature[]
|
|
101
111
|
/** Flash size in bytes (0 on host) */
|
|
102
112
|
flash: number
|
|
103
113
|
/** PSRAM size in bytes (0 if unavailable) */
|
package/runtime/test/test.ts
CHANGED
|
@@ -510,13 +510,17 @@ async function run(): Promise<void> {
|
|
|
510
510
|
} catch (e) {
|
|
511
511
|
const msg = formatThrown(e)
|
|
512
512
|
emit({e: 7, s: suite.name, m: msg})
|
|
513
|
+
// Fail the suite's tests rather than skip them: a broken beforeAll is
|
|
514
|
+
// a broken run, and skipping made it indistinguishable from a
|
|
515
|
+
// deliberate gate. The e:7 above carries the real error; the per-test
|
|
516
|
+
// message just points at it.
|
|
513
517
|
for (const t of suite.tests) {
|
|
514
518
|
if (t.todo) {
|
|
515
519
|
emit({e: 9, s: suite.name, t: t.name})
|
|
516
520
|
todo++
|
|
517
521
|
} else {
|
|
518
|
-
emit({e:
|
|
519
|
-
|
|
522
|
+
emit({e: 3, s: suite.name, t: t.name, d: 0, m: 'beforeAll failed'})
|
|
523
|
+
failed++
|
|
520
524
|
}
|
|
521
525
|
}
|
|
522
526
|
emit({e: 5, s: suite.name})
|
package/runtime/wifi/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/* Type surface for mikro/wifi. On-device the module is the native
|
|
2
|
+
* implementation in src/mik_wifi_client.cpp (behavior pinned by
|
|
3
|
+
* test/wifi_client_test.cpp). */
|
|
1
4
|
import type {Observable} from '../observable/types.js'
|
|
2
5
|
import type {Result} from '../result/types.js'
|
|
3
6
|
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema conformance fixtures: every case is run through the TypeScript
|
|
3
|
+
* implementation here and its outcome recorded, then replayed through the
|
|
4
|
+
* native module by test/schema_conformance_test.cpp.
|
|
5
|
+
*
|
|
6
|
+
* `mikro/schema` has two implementations of one contract: @mikrojs/schema
|
|
7
|
+
* (host: the CLI evaluating mikro.config.ts, the registry, vitest) and
|
|
8
|
+
* src/mik_schema.cpp (device). Two independent suites are not enough to keep
|
|
9
|
+
* them together — the check-in wire had exactly this shape and drifted while
|
|
10
|
+
* both suites stayed green (see test/ota_wire_fixtures_test.cpp). These
|
|
11
|
+
* fixtures make the C++ answer the same questions the TypeScript answered.
|
|
12
|
+
*
|
|
13
|
+
* The recorded outcome is whatever core.ts does, bugs included. That is the
|
|
14
|
+
* point: this file proves the two agree, and the vitest suite proves core.ts is
|
|
15
|
+
* right. Regenerated on every test build so it cannot lag.
|
|
16
|
+
*
|
|
17
|
+
* Usage: node gen-schema-fixtures.js <outdir>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {mkdirSync, writeFileSync} from 'node:fs'
|
|
21
|
+
import {join} from 'node:path'
|
|
22
|
+
|
|
23
|
+
/* The source, not the package specifier: Node would resolve @mikrojs/schema
|
|
24
|
+
* through its `import` condition to dist and make this need a build first. */
|
|
25
|
+
const s = await import('../../schema/src/core.ts')
|
|
26
|
+
|
|
27
|
+
const outDir = process.argv[2]
|
|
28
|
+
if (!outDir) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.error('Usage: gen-schema-fixtures.js <outdir>')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── Cases ─────────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
/* Constructor calls: the driver does S[call](...args) and compares the node it
|
|
37
|
+
* builds, or the TypeError message it throws. Args are plain data because a
|
|
38
|
+
* schema is plain data — that is what makes the same list drivable from C++. */
|
|
39
|
+
const CONSTRUCT_CASES = [
|
|
40
|
+
['string()', 'string', []],
|
|
41
|
+
['string with annotations', 'string', [{title: 'Name', description: 'Who', default: 'a'}]],
|
|
42
|
+
['string with constraints', 'string', [{minLength: 1, maxLength: 8, format: 'email'}]],
|
|
43
|
+
['string masked', 'string', [{mask: true}]],
|
|
44
|
+
['number()', 'number', []],
|
|
45
|
+
['number with bounds', 'number', [{min: 0, max: 10, integer: true, unit: 'ms'}]],
|
|
46
|
+
['number with default', 'number', [{default: 5}]],
|
|
47
|
+
['boolean with default', 'boolean', [{default: true}]],
|
|
48
|
+
['unknown()', 'unknown', []],
|
|
49
|
+
['literal string', 'literal', ['on']],
|
|
50
|
+
['literal number with title', 'literal', [3, {title: 'Three'}]],
|
|
51
|
+
['literal boolean', 'literal', [false]],
|
|
52
|
+
['array of numbers', 'array', [{kind: 'number'}]],
|
|
53
|
+
['array with item bounds', 'array', [{kind: 'string'}, {minItems: 1, maxItems: 4}]],
|
|
54
|
+
['array with default', 'array', [{kind: 'number'}, {default: [1, 2]}]],
|
|
55
|
+
['object empty', 'object', [{}]],
|
|
56
|
+
['object with fields', 'object', [{a: {kind: 'number'}, b: {kind: 'string'}}]],
|
|
57
|
+
['object with title', 'object', [{a: {kind: 'number'}}, {title: 'Group'}]],
|
|
58
|
+
['optional wraps', 'optional', [{kind: 'number'}]],
|
|
59
|
+
['tuple', 'tuple', [[{kind: 'number'}, {kind: 'string'}]]],
|
|
60
|
+
['union', 'union', [[{kind: 'number'}, {kind: 'string'}]]],
|
|
61
|
+
[
|
|
62
|
+
'enumOf',
|
|
63
|
+
'enumOf',
|
|
64
|
+
[
|
|
65
|
+
[
|
|
66
|
+
{value: 1, title: 'One'},
|
|
67
|
+
{value: 2, description: 'Two'},
|
|
68
|
+
],
|
|
69
|
+
],
|
|
70
|
+
],
|
|
71
|
+
['enumOf with default', 'enumOf', [[{value: 'a'}, {value: 'b'}], {default: 'b'}]],
|
|
72
|
+
[
|
|
73
|
+
'taggedUnion',
|
|
74
|
+
'taggedUnion',
|
|
75
|
+
['type', {a: {kind: 'object', shape: {}}, b: {kind: 'object', shape: {}}}],
|
|
76
|
+
],
|
|
77
|
+
|
|
78
|
+
// Authoring rejections. These are TypeErrors thrown where the schema is
|
|
79
|
+
// written, and they are as much a part of the contract as validation is.
|
|
80
|
+
['object rejects a default', 'object', [{}, {default: {}}]],
|
|
81
|
+
['optional rejects a defaulted inner', 'optional', [{kind: 'number', default: 1}]],
|
|
82
|
+
['array rejects an inner default', 'array', [{kind: 'number', default: 1}]],
|
|
83
|
+
[
|
|
84
|
+
'array rejects a nested inner default',
|
|
85
|
+
'array',
|
|
86
|
+
[{kind: 'object', shape: {a: {kind: 'number', default: 1}}}],
|
|
87
|
+
],
|
|
88
|
+
['tuple rejects an inner default', 'tuple', [[{kind: 'number', default: 1}]]],
|
|
89
|
+
['union rejects an inner default', 'union', [[{kind: 'number', default: 1}]]],
|
|
90
|
+
[
|
|
91
|
+
'taggedUnion rejects an inner default',
|
|
92
|
+
'taggedUnion',
|
|
93
|
+
['t', {a: {kind: 'object', shape: {x: {kind: 'number', default: 1}}}}],
|
|
94
|
+
],
|
|
95
|
+
['default must match the node', 'number', [{default: 'nope'}]],
|
|
96
|
+
['array default must match', 'array', [{kind: 'number'}, {default: ['nope']}]],
|
|
97
|
+
['default must satisfy its own bound', 'number', [{min: 10, default: 5}]],
|
|
98
|
+
['default within its bound is fine', 'number', [{min: 1, max: 10, default: 5}]],
|
|
99
|
+
['default must satisfy minLength', 'string', [{minLength: 3, default: 'ab'}]],
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
/* Validation: schemas are built with the constructors for readability and
|
|
103
|
+
* serialized as data, which is what the device receives anyway. */
|
|
104
|
+
const V = {
|
|
105
|
+
str: s.string(),
|
|
106
|
+
num: s.number(),
|
|
107
|
+
bool: s.boolean(),
|
|
108
|
+
unk: s.unknown(),
|
|
109
|
+
litOn: s.literal('on'),
|
|
110
|
+
lit3: s.literal(3),
|
|
111
|
+
arrNum: s.array(s.number()),
|
|
112
|
+
obj: s.object({a: s.number(), b: s.string()}),
|
|
113
|
+
objOpt: s.object({a: s.number(), b: s.optional(s.string())}),
|
|
114
|
+
nested: s.object({outer: s.object({inner: s.array(s.number())})}),
|
|
115
|
+
tup: s.tuple([s.number(), s.string()]),
|
|
116
|
+
uni: s.union([s.number(), s.string()]),
|
|
117
|
+
tagged: s.taggedUnion('type', {
|
|
118
|
+
move: s.object({x: s.number()}),
|
|
119
|
+
stop: s.object({}),
|
|
120
|
+
}),
|
|
121
|
+
optNum: s.optional(s.number()),
|
|
122
|
+
bounded: s.number({min: 0, max: 10}),
|
|
123
|
+
whole: s.number({integer: true}),
|
|
124
|
+
fractional: s.number({min: 0.5, max: 2.5}),
|
|
125
|
+
sized: s.string({minLength: 2, maxLength: 4}),
|
|
126
|
+
items: s.array(s.number(), {minItems: 1, maxItems: 3}),
|
|
127
|
+
emailed: s.string({format: 'email'}),
|
|
128
|
+
boundedObj: s.object({pin: s.number({min: 0, max: 30, integer: true})}),
|
|
129
|
+
/* The rule the two-pass arrangement got wrong: a union accepts what ANY
|
|
130
|
+
* member accepts, so 150 has to pass on the second member even though it
|
|
131
|
+
* matches the first member's shape and fails its bound. */
|
|
132
|
+
splitRange: s.union([s.number({max: 10}), s.number({min: 100})]),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const VALIDATE_CASES = [
|
|
136
|
+
['string accepts', V.str, 'hello'],
|
|
137
|
+
['string rejects a number', V.str, 42],
|
|
138
|
+
['string rejects null', V.str, null],
|
|
139
|
+
['string rejects undefined', V.str, undefined],
|
|
140
|
+
['number accepts', V.num, 1.5],
|
|
141
|
+
['number accepts a negative', V.num, -7],
|
|
142
|
+
['number rejects NaN', V.num, NaN],
|
|
143
|
+
['number rejects a string', V.num, '1'],
|
|
144
|
+
['boolean accepts', V.bool, false],
|
|
145
|
+
['boolean rejects 0', V.bool, 0],
|
|
146
|
+
['unknown accepts a string', V.unk, 'x'],
|
|
147
|
+
['unknown accepts undefined', V.unk, undefined],
|
|
148
|
+
['unknown accepts an object', V.unk, {a: 1}],
|
|
149
|
+
['literal accepts its value', V.litOn, 'on'],
|
|
150
|
+
['literal rejects another string', V.litOn, 'off'],
|
|
151
|
+
['literal number accepts', V.lit3, 3],
|
|
152
|
+
['literal number rejects', V.lit3, 4],
|
|
153
|
+
['literal reports undefined', V.lit3, undefined],
|
|
154
|
+
['array accepts', V.arrNum, [1, 2, 3]],
|
|
155
|
+
['array accepts empty', V.arrNum, []],
|
|
156
|
+
['array rejects a non-array', V.arrNum, 'no'],
|
|
157
|
+
['array rejects an object', V.arrNum, {}],
|
|
158
|
+
['array reports the bad index', V.arrNum, [1, 'two', 3]],
|
|
159
|
+
['object accepts', V.obj, {a: 1, b: 'x'}],
|
|
160
|
+
['object ignores extra keys', V.obj, {a: 1, b: 'x', extra: true}],
|
|
161
|
+
['object reports a missing field', V.obj, {a: 1}],
|
|
162
|
+
['object reports a bad field', V.obj, {a: 1, b: 2}],
|
|
163
|
+
['object rejects an array', V.obj, [1, 2]],
|
|
164
|
+
['object rejects null', V.obj, null],
|
|
165
|
+
['optional field may be absent', V.objOpt, {a: 1}],
|
|
166
|
+
['optional field may be present', V.objOpt, {a: 1, b: 'x'}],
|
|
167
|
+
['optional field still typechecked', V.objOpt, {a: 1, b: 2}],
|
|
168
|
+
['nested reports a deep path', V.nested, {outer: {inner: [1, 'no']}}],
|
|
169
|
+
['nested accepts', V.nested, {outer: {inner: []}}],
|
|
170
|
+
['tuple accepts', V.tup, [1, 'x']],
|
|
171
|
+
['tuple rejects a short one', V.tup, [1]],
|
|
172
|
+
['tuple rejects a long one', V.tup, [1, 'x', 3]],
|
|
173
|
+
['tuple reports a bad position', V.tup, ['x', 'x']],
|
|
174
|
+
['union accepts the first', V.uni, 1],
|
|
175
|
+
['union accepts the second', V.uni, 'x'],
|
|
176
|
+
['union rejects neither', V.uni, true],
|
|
177
|
+
['taggedUnion accepts a branch', V.tagged, {type: 'move', x: 1}],
|
|
178
|
+
['taggedUnion accepts the empty branch', V.tagged, {type: 'stop'}],
|
|
179
|
+
['taggedUnion reports a missing tag', V.tagged, {x: 1}],
|
|
180
|
+
['taggedUnion reports an unknown tag', V.tagged, {type: 'spin'}],
|
|
181
|
+
['taggedUnion reports a non-primitive tag', V.tagged, {type: {}}],
|
|
182
|
+
['taggedUnion validates the branch', V.tagged, {type: 'move', x: 'no'}],
|
|
183
|
+
['taggedUnion rejects an array', V.tagged, []],
|
|
184
|
+
['bare optional accepts undefined', V.optNum, undefined],
|
|
185
|
+
['bare optional accepts a value', V.optNum, 1],
|
|
186
|
+
['bare optional rejects a bad value', V.optNum, 'x'],
|
|
187
|
+
|
|
188
|
+
/* Bounds. These used to be host-only; parse() now enforces everything except
|
|
189
|
+
* format and unit wherever it runs. */
|
|
190
|
+
['min accepts the boundary', V.bounded, 0],
|
|
191
|
+
['max accepts the boundary', V.bounded, 10],
|
|
192
|
+
['below min rejected', V.bounded, -1],
|
|
193
|
+
['above max rejected', V.bounded, 11],
|
|
194
|
+
['integer accepts a whole number', V.whole, 4],
|
|
195
|
+
['integer rejects a fraction', V.whole, 4.5],
|
|
196
|
+
['integer rejects Infinity', V.whole, Infinity],
|
|
197
|
+
['fractional bounds accept', V.fractional, 1.25],
|
|
198
|
+
['fractional bounds report the bound as written', V.fractional, 0.25],
|
|
199
|
+
['minLength accepts the boundary', V.sized, 'ab'],
|
|
200
|
+
['maxLength accepts the boundary', V.sized, 'abcd'],
|
|
201
|
+
['shorter than minLength rejected', V.sized, 'a'],
|
|
202
|
+
['longer than maxLength rejected', V.sized, 'abcde'],
|
|
203
|
+
['length counts UTF-16 units', V.sized, '\u{1F600}'],
|
|
204
|
+
['minItems accepts the boundary', V.items, [1]],
|
|
205
|
+
['maxItems accepts the boundary', V.items, [1, 2, 3]],
|
|
206
|
+
['fewer than minItems rejected', V.items, []],
|
|
207
|
+
['more than maxItems rejected', V.items, [1, 2, 3, 4]],
|
|
208
|
+
['length is checked before elements', V.items, [1, 2, 3, 'no']],
|
|
209
|
+
['bounds report the field path', V.boundedObj, {pin: 200}],
|
|
210
|
+
['format is not checked here', V.emailed, 'not-an-email'],
|
|
211
|
+
['a union member failing its bound falls through', V.splitRange, 150],
|
|
212
|
+
['a union with no member in range is rejected', V.splitRange, 50],
|
|
213
|
+
|
|
214
|
+
/* Prototype-chain hazards. core.ts uses Object.hasOwn at three sites so an
|
|
215
|
+
* inherited member cannot stand in for a field or a branch; JS_GetPropertyStr
|
|
216
|
+
* walks the prototype chain and would reopen exactly this. */
|
|
217
|
+
['object does not read an inherited field', s.object({constructor: s.number()}), {}],
|
|
218
|
+
[
|
|
219
|
+
'object accepts a real constructor field',
|
|
220
|
+
s.object({constructor: s.number()}),
|
|
221
|
+
{constructor: 1},
|
|
222
|
+
],
|
|
223
|
+
['taggedUnion does not dispatch on an inherited tag', V.tagged, {type: 'constructor'}],
|
|
224
|
+
['taggedUnion does not dispatch on toString', V.tagged, {type: 'toString'}],
|
|
225
|
+
['object does not read an inherited __proto__', s.object({a: s.number()}), {}],
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
const APPLY_DEFAULTS_CASES = [
|
|
229
|
+
['fills a scalar default', s.object({a: s.number({default: 5})}), undefined],
|
|
230
|
+
['keeps a present value', s.object({a: s.number({default: 5})}), {a: 9}],
|
|
231
|
+
['drops unknown keys', s.object({a: s.number({default: 5})}), {a: 1, z: 2}],
|
|
232
|
+
['leaves an optional absent', s.object({a: s.optional(s.number())}), {}],
|
|
233
|
+
['keeps a present optional', s.object({a: s.optional(s.number())}), {a: 1}],
|
|
234
|
+
['recurses into nested objects', s.object({o: s.object({a: s.number({default: 2})})}), {}],
|
|
235
|
+
['array default fills', s.object({a: s.array(s.number(), {default: [1]})}), {}],
|
|
236
|
+
['array without a default is empty', s.object({a: s.array(s.number())}), {}],
|
|
237
|
+
['a present non-object stays as-is', s.object({a: s.number()}), 42],
|
|
238
|
+
['does not fill through an inherited key', s.object({constructor: s.number({default: 1})}), {}],
|
|
239
|
+
['unknown passes through', s.unknown(), undefined],
|
|
240
|
+
['scalar default at the root', s.number({default: 3}), undefined],
|
|
241
|
+
/* Hand-built AST: the constructors reject a default that breaks its own
|
|
242
|
+
* bound, but a schema that arrived as JSON ran none of them, and
|
|
243
|
+
* applyDefaults must still fill without validating. */
|
|
244
|
+
[
|
|
245
|
+
'a default outside its own bound still fills',
|
|
246
|
+
{kind: 'object', shape: {a: {kind: 'number', min: 10, default: 5}}},
|
|
247
|
+
{},
|
|
248
|
+
],
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
/* ── Recording ─────────────────────────────────────────────────────── */
|
|
252
|
+
|
|
253
|
+
function recordConstruct([name, call, args]) {
|
|
254
|
+
try {
|
|
255
|
+
return {name, kind: 'construct', call, args, expect: s[call](...args)}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
return {name, kind: 'construct', call, args, throws: e.message}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function recordValidate([name, schema, value]) {
|
|
262
|
+
const result = s.validate(schema, value, '')
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
kind: 'validate',
|
|
266
|
+
schema,
|
|
267
|
+
value,
|
|
268
|
+
expect: result === null ? null : {message: result.error.message, path: result.error.path},
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function recordApplyDefaults([name, schema, value]) {
|
|
273
|
+
return {name, kind: 'applyDefaults', schema, value, expect: s.applyDefaults(schema, value)}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ── Emitting ──────────────────────────────────────────────────────── */
|
|
277
|
+
|
|
278
|
+
/* JS source, not JSON: the cases carry undefined and NaN, which JSON cannot
|
|
279
|
+
* represent and which are exactly the values the edge cases are about. */
|
|
280
|
+
function js(value) {
|
|
281
|
+
if (value === undefined) return 'undefined'
|
|
282
|
+
if (value === null) return 'null'
|
|
283
|
+
if (typeof value === 'number') {
|
|
284
|
+
if (Number.isNaN(value)) return 'NaN'
|
|
285
|
+
if (value === Infinity) return 'Infinity'
|
|
286
|
+
if (value === -Infinity) return '-Infinity'
|
|
287
|
+
return String(value)
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value)
|
|
290
|
+
if (Array.isArray(value)) return `[${value.map(js).join(',')}]`
|
|
291
|
+
if (typeof value === 'object') {
|
|
292
|
+
const body = Object.keys(value)
|
|
293
|
+
.map((k) => `${JSON.stringify(k)}:${js(value[k])}`)
|
|
294
|
+
.join(',')
|
|
295
|
+
return `{${body}}`
|
|
296
|
+
}
|
|
297
|
+
throw new TypeError(`cannot serialize ${typeof value}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const cases = [
|
|
301
|
+
...CONSTRUCT_CASES.map(recordConstruct),
|
|
302
|
+
...VALIDATE_CASES.map(recordValidate),
|
|
303
|
+
...APPLY_DEFAULTS_CASES.map(recordApplyDefaults),
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
const body = cases.map((c) => ` ${js(c)},`).join('\n')
|
|
307
|
+
const source = `/* GENERATED by scripts/gen-schema-fixtures.js. Do not edit. */
|
|
308
|
+
const cases = [
|
|
309
|
+
${body}
|
|
310
|
+
]
|
|
311
|
+
`
|
|
312
|
+
|
|
313
|
+
mkdirSync(outDir, {recursive: true})
|
|
314
|
+
writeFileSync(join(outDir, 'schema-fixtures.js'), source)
|
|
315
|
+
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
317
|
+
console.log(`schema fixtures: ${cases.length} cases -> ${join(outDir, 'schema-fixtures.js')}`)
|