@mikrojs/native 0.18.3-next.20260829153835 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +24 -2
- 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 +28 -0
- package/runtime/ota/types.ts +3 -3
- package/runtime/schema/schema.ts +16 -3
- package/runtime/schema/types.ts +1 -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/mik_app_config.cpp +9 -1
- package/src/mik_http_client.cpp +1357 -0
- package/src/mik_repl.cpp +44 -5
- package/src/mik_schema.cpp +892 -0
- 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 -163
- package/dist/runtime/schema/core.d.ts.map +0 -1
- package/dist/runtime/schema/core.js +0 -306
- package/dist/runtime/schema/core.js.map +0 -1
- package/dist/runtime/schema/shared.d.ts +0 -79
- package/dist/runtime/schema/shared.d.ts.map +0 -1
- package/dist/runtime/schema/shared.js +0 -1005
- 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 -842
- package/runtime/schema/shared.ts +0 -1040
- package/runtime/wifi/wifi.ts +0 -191
package/runtime/schema/core.ts
DELETED
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
/* Core schema machinery: types, constructors, the validator, and
|
|
2
|
-
* applyDefaults. Dependency-free so hosts (CLI, registries) can import it via
|
|
3
|
-
* shared.ts without resolving mikro/* builtins; mikro/schema re-exports it and
|
|
4
|
-
* adds the Result-returning parse(). */
|
|
5
|
-
|
|
6
|
-
function err<E>(error: E) {
|
|
7
|
-
return {ok: false as const, error}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/* Declared outright rather than derived from the factory with ReturnType. The
|
|
11
|
-
* `as const` this replaces existed for the `name` literal type, but it also made
|
|
12
|
-
* every field readonly, which stopped the type reducing against structurally
|
|
13
|
-
* equal error unions elsewhere (kv's KVError carries the same ValidationFailed
|
|
14
|
-
* shape) and broke contextual typing at those call sites. */
|
|
15
|
-
export type SchemaError = {name: 'ValidationFailed'; message: string; path: string}
|
|
16
|
-
export const SchemaError = {
|
|
17
|
-
ValidationFailed: (message: string, path: string): SchemaError => ({
|
|
18
|
-
name: 'ValidationFailed',
|
|
19
|
-
message,
|
|
20
|
-
path,
|
|
21
|
-
}),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ── Schema types ────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
type Primitive = string | number | boolean
|
|
27
|
-
|
|
28
|
-
/* A closed set of string shapes, deliberately not a caller-supplied regex: a
|
|
29
|
-
* registry validates operator input against a published schema, so a pattern
|
|
30
|
-
* from a publisher would be a denial-of-service vector on the registry (one
|
|
31
|
-
* catastrophic-backtracking expression from anyone who can publish). These are
|
|
32
|
-
* ours, fixed, and linear.
|
|
33
|
-
*
|
|
34
|
-
* `url` means any parseable absolute URL with a scheme, not http and https
|
|
35
|
-
* only: mqtt:// and ws:// are ordinary device-config values. A scheme
|
|
36
|
-
* allowlist is not expressible, which is the gap that would justify extending
|
|
37
|
-
* this. ipv6 is deliberately absent: no demand, and it is the one shape whose
|
|
38
|
-
* check is large enough to be worth its own decision. */
|
|
39
|
-
export type Format = 'url' | 'hostname' | 'ipv4' | 'mac' | 'email'
|
|
40
|
-
|
|
41
|
-
/* The unit an operator sees beside a number, and the one the app reads: the
|
|
42
|
-
* annotation describes the stored value and never converts it.
|
|
43
|
-
*
|
|
44
|
-
* The set is the IANA SenML Units and Secondary Units registries, which is the
|
|
45
|
-
* right basis rather than an invention of ours: ASCII by construction, scoped
|
|
46
|
-
* to constrained devices, and already built on the two-tier model this needs,
|
|
47
|
-
* where a secondary unit derives from a primary by scale and offset. Adopted
|
|
48
|
-
* wholesale, minus the entries SenML marks NOT RECOMMENDED for new producers
|
|
49
|
-
* (we are a new producer), plus the microcontroller units its secondary
|
|
50
|
-
* registry lacks -- it is telemetry-shaped, so it has no us, kHz, mW, uA, mAh,
|
|
51
|
-
* MiB, kohm or Bd.
|
|
52
|
-
*
|
|
53
|
-
* Two deliberate departures, both documented in docs/registry-spec.md:
|
|
54
|
-
* `deg` is kept despite its NOT RECOMMENDED marking, because an operator types
|
|
55
|
-
* degrees and not radians; `d` for day is dropped, because a bare `d` is the
|
|
56
|
-
* SI deci- prefix and RFC 8428 guideline 7 forbids standalone prefix letters.
|
|
57
|
-
*
|
|
58
|
-
* Note SenML's `%` is NOT a percentage -- it is a synonym for the ratio `/`,
|
|
59
|
-
* and the RFC says so explicitly. It is excluded, so a 0-100 field uses `/100`,
|
|
60
|
-
* which a form renders as `%`. */
|
|
61
|
-
export type Unit =
|
|
62
|
-
| 'm'
|
|
63
|
-
| 'kg'
|
|
64
|
-
| 's'
|
|
65
|
-
| 'A'
|
|
66
|
-
| 'K'
|
|
67
|
-
| 'cd'
|
|
68
|
-
| 'mol'
|
|
69
|
-
| 'Hz'
|
|
70
|
-
| 'rad'
|
|
71
|
-
| 'sr'
|
|
72
|
-
| 'N'
|
|
73
|
-
| 'Pa'
|
|
74
|
-
| 'J'
|
|
75
|
-
| 'W'
|
|
76
|
-
| 'C'
|
|
77
|
-
| 'V'
|
|
78
|
-
| 'F'
|
|
79
|
-
| 'Ohm'
|
|
80
|
-
| 'S'
|
|
81
|
-
| 'Wb'
|
|
82
|
-
| 'T'
|
|
83
|
-
| 'H'
|
|
84
|
-
| 'Cel'
|
|
85
|
-
| 'lm'
|
|
86
|
-
| 'lx'
|
|
87
|
-
| 'Bq'
|
|
88
|
-
| 'Gy'
|
|
89
|
-
| 'Sv'
|
|
90
|
-
| 'kat'
|
|
91
|
-
| 'm2'
|
|
92
|
-
| 'm3'
|
|
93
|
-
| 'm/s'
|
|
94
|
-
| 'm/s2'
|
|
95
|
-
| 'm3/s'
|
|
96
|
-
| 'W/m2'
|
|
97
|
-
| 'cd/m2'
|
|
98
|
-
| 'bit'
|
|
99
|
-
| 'bit/s'
|
|
100
|
-
| 'lat'
|
|
101
|
-
| 'lon'
|
|
102
|
-
| 'pH'
|
|
103
|
-
| 'dB'
|
|
104
|
-
| 'dBW'
|
|
105
|
-
| 'count'
|
|
106
|
-
| '/'
|
|
107
|
-
| '%RH'
|
|
108
|
-
| '%EL'
|
|
109
|
-
| 'EL'
|
|
110
|
-
| '1/s'
|
|
111
|
-
| 'S/m'
|
|
112
|
-
| 'B'
|
|
113
|
-
| 'VA'
|
|
114
|
-
| 'VAs'
|
|
115
|
-
| 'var'
|
|
116
|
-
| 'vars'
|
|
117
|
-
| 'J/m'
|
|
118
|
-
| 'kg/m3'
|
|
119
|
-
| 'deg'
|
|
120
|
-
| 'NTU'
|
|
121
|
-
| 'ms'
|
|
122
|
-
| 'min'
|
|
123
|
-
| 'h'
|
|
124
|
-
| 'MHz'
|
|
125
|
-
| 'kW'
|
|
126
|
-
| 'kVA'
|
|
127
|
-
| 'kvar'
|
|
128
|
-
| 'Ah'
|
|
129
|
-
| 'Wh'
|
|
130
|
-
| 'kWh'
|
|
131
|
-
| 'varh'
|
|
132
|
-
| 'kvarh'
|
|
133
|
-
| 'kVAh'
|
|
134
|
-
| 'Wh/km'
|
|
135
|
-
| 'KiB'
|
|
136
|
-
| 'GB'
|
|
137
|
-
| 'Mbit/s'
|
|
138
|
-
| 'B/s'
|
|
139
|
-
| 'MB/s'
|
|
140
|
-
| 'mV'
|
|
141
|
-
| 'mA'
|
|
142
|
-
| 'dBm'
|
|
143
|
-
| 'ug/m3'
|
|
144
|
-
| 'mm/h'
|
|
145
|
-
| 'm/h'
|
|
146
|
-
| 'ppm'
|
|
147
|
-
| '/100'
|
|
148
|
-
| '/1000'
|
|
149
|
-
| 'hPa'
|
|
150
|
-
| 'mm'
|
|
151
|
-
| 'cm'
|
|
152
|
-
| 'km'
|
|
153
|
-
| 'km/h'
|
|
154
|
-
| 'ppb'
|
|
155
|
-
| 'ppt'
|
|
156
|
-
| 'VAh'
|
|
157
|
-
| 'mg/l'
|
|
158
|
-
| 'ug/l'
|
|
159
|
-
| 'g/l'
|
|
160
|
-
| 'us'
|
|
161
|
-
| 'kHz'
|
|
162
|
-
| 'GHz'
|
|
163
|
-
| 'mW'
|
|
164
|
-
| 'uA'
|
|
165
|
-
| 'uV'
|
|
166
|
-
| 'mAh'
|
|
167
|
-
| 'MiB'
|
|
168
|
-
| 'kB'
|
|
169
|
-
| 'MB'
|
|
170
|
-
| 'kbit/s'
|
|
171
|
-
| 'KiB/s'
|
|
172
|
-
| 'kohm'
|
|
173
|
-
| 'Mohm'
|
|
174
|
-
| 'kPa'
|
|
175
|
-
| 'bar'
|
|
176
|
-
| 'Bd'
|
|
177
|
-
|
|
178
|
-
/* The `default` annotation is stored as an extra node property so a schema
|
|
179
|
-
* serializes to JSON as-is; it is typed precisely on the constructor options
|
|
180
|
-
* and loosely on the node, which keeps Infer free of recursive
|
|
181
|
-
* instantiations. */
|
|
182
|
-
|
|
183
|
-
/* Display annotations, carried by every node a form can render. They never
|
|
184
|
-
* change what validates, so a consumer that does not render a form ignores
|
|
185
|
-
* them. Not on optional(): the wrapper expresses absence, the node it wraps
|
|
186
|
-
* expresses identity, so annotations go on the inner. */
|
|
187
|
-
interface Annotated {
|
|
188
|
-
readonly title?: string
|
|
189
|
-
readonly description?: string
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface StringSchema extends Annotated {
|
|
193
|
-
readonly kind: 'string'
|
|
194
|
-
readonly default?: string
|
|
195
|
-
readonly mask?: boolean
|
|
196
|
-
readonly minLength?: number
|
|
197
|
-
readonly maxLength?: number
|
|
198
|
-
readonly format?: Format
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface NumberSchema extends Annotated {
|
|
202
|
-
readonly kind: 'number'
|
|
203
|
-
readonly default?: number
|
|
204
|
-
readonly mask?: boolean
|
|
205
|
-
readonly min?: number
|
|
206
|
-
readonly max?: number
|
|
207
|
-
readonly integer?: boolean
|
|
208
|
-
readonly unit?: Unit
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export interface BooleanSchema extends Annotated {
|
|
212
|
-
readonly kind: 'boolean'
|
|
213
|
-
readonly default?: boolean
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export interface UnknownSchema {
|
|
217
|
-
readonly kind: 'unknown'
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface LiteralSchema<T extends Primitive = Primitive> extends Annotated {
|
|
221
|
-
readonly kind: 'literal'
|
|
222
|
-
readonly value: T
|
|
223
|
-
readonly default?: T
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface ArraySchema<S extends Schema = Schema> extends Annotated {
|
|
227
|
-
readonly kind: 'array'
|
|
228
|
-
readonly element: S
|
|
229
|
-
readonly default?: unknown
|
|
230
|
-
readonly minItems?: number
|
|
231
|
-
readonly maxItems?: number
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface ObjectSchema<
|
|
235
|
-
Shape extends Record<string, Schema> = Record<string, Schema>,
|
|
236
|
-
> extends Annotated {
|
|
237
|
-
readonly kind: 'object'
|
|
238
|
-
readonly shape: Shape
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export interface OptionalSchema<S extends Schema = Schema> {
|
|
242
|
-
readonly kind: 'optional'
|
|
243
|
-
readonly inner: S
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export interface TupleSchema<
|
|
247
|
-
Elements extends readonly Schema[] = readonly Schema[],
|
|
248
|
-
> extends Annotated {
|
|
249
|
-
readonly kind: 'tuple'
|
|
250
|
-
readonly elements: Elements
|
|
251
|
-
readonly default?: unknown
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export interface UnionSchema<
|
|
255
|
-
Members extends readonly Schema[] = readonly Schema[],
|
|
256
|
-
> extends Annotated {
|
|
257
|
-
readonly kind: 'union'
|
|
258
|
-
readonly members: Members
|
|
259
|
-
readonly default?: unknown
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export interface TaggedUnionSchema<
|
|
263
|
-
Key extends string = string,
|
|
264
|
-
Branches extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,
|
|
265
|
-
> extends Annotated {
|
|
266
|
-
readonly kind: 'taggedUnion'
|
|
267
|
-
readonly key: Key
|
|
268
|
-
readonly branches: Branches
|
|
269
|
-
readonly default?: unknown
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export type Schema =
|
|
273
|
-
| StringSchema
|
|
274
|
-
| NumberSchema
|
|
275
|
-
| BooleanSchema
|
|
276
|
-
| UnknownSchema
|
|
277
|
-
| LiteralSchema
|
|
278
|
-
| ArraySchema
|
|
279
|
-
| ObjectSchema
|
|
280
|
-
| OptionalSchema
|
|
281
|
-
| TupleSchema
|
|
282
|
-
| UnionSchema
|
|
283
|
-
| TaggedUnionSchema
|
|
284
|
-
|
|
285
|
-
// ── Type inference ──────────────────────────────────────────────────
|
|
286
|
-
|
|
287
|
-
type Simplify<T> = {[K in keyof T]: T[K]} & {}
|
|
288
|
-
|
|
289
|
-
export type Infer<S> = S extends StringSchema
|
|
290
|
-
? string
|
|
291
|
-
: S extends NumberSchema
|
|
292
|
-
? number
|
|
293
|
-
: S extends BooleanSchema
|
|
294
|
-
? boolean
|
|
295
|
-
: S extends UnknownSchema
|
|
296
|
-
? unknown
|
|
297
|
-
: S extends LiteralSchema<infer T>
|
|
298
|
-
? T
|
|
299
|
-
: S extends ArraySchema<infer E>
|
|
300
|
-
? ArraySchema extends S
|
|
301
|
-
? []
|
|
302
|
-
: Infer<E>[]
|
|
303
|
-
: S extends ObjectSchema<infer Shape>
|
|
304
|
-
? ObjectSchema extends S
|
|
305
|
-
? object
|
|
306
|
-
: Simplify<InferObject<Shape>>
|
|
307
|
-
: S extends TupleSchema<infer Elements>
|
|
308
|
-
? InferTuple<Elements>
|
|
309
|
-
: S extends OptionalSchema<infer Inner>
|
|
310
|
-
? OptionalSchema extends S
|
|
311
|
-
? OptionalSchema
|
|
312
|
-
: Infer<Inner> | undefined
|
|
313
|
-
: S extends UnionSchema<infer Members>
|
|
314
|
-
? InferUnion<Members>
|
|
315
|
-
: S extends TaggedUnionSchema<infer Key, infer Branches>
|
|
316
|
-
? InferTaggedUnion<Key, Branches>
|
|
317
|
-
: never
|
|
318
|
-
|
|
319
|
-
type InferObject<Shape> = {
|
|
320
|
-
[K in keyof Shape as Shape[K] extends OptionalSchema ? never : K]: Infer<Shape[K]>
|
|
321
|
-
} & {
|
|
322
|
-
[K in keyof Shape as Shape[K] extends OptionalSchema ? K : never]?: Infer<Shape[K]>
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
type InferTuple<Elements> = Elements extends readonly [infer Head, ...infer Tail]
|
|
326
|
-
? [Infer<Head>, ...InferTuple<Tail>]
|
|
327
|
-
: []
|
|
328
|
-
|
|
329
|
-
type InferUnion<Members> = Members extends readonly [infer Head, ...infer Tail]
|
|
330
|
-
? Infer<Head> | InferUnion<Tail>
|
|
331
|
-
: never
|
|
332
|
-
|
|
333
|
-
type InferTaggedUnion<Key extends string, Branches> = {
|
|
334
|
-
[Tag in keyof Branches & string]: {[K in Key]: Tag} & Infer<Branches[Tag]>
|
|
335
|
-
}[keyof Branches & string]
|
|
336
|
-
|
|
337
|
-
/* The read type: what applyDefaults alone can hand back. A field defaults
|
|
338
|
-
* cannot fill is optional here, while Infer keeps it required: Infer is the
|
|
339
|
-
* write type, where an operator must supply it. */
|
|
340
|
-
export type InferRead<S> =
|
|
341
|
-
S extends ObjectSchema<infer Shape>
|
|
342
|
-
? ObjectSchema extends S
|
|
343
|
-
? object
|
|
344
|
-
: Simplify<InferReadObject<Shape>>
|
|
345
|
-
: Infer<S>
|
|
346
|
-
|
|
347
|
-
/* What the materialized defaults always contain: a node carrying its own
|
|
348
|
-
* default, or a plain object whose fields ALL fill (or are optional). A
|
|
349
|
-
* defaultless array and a partially fillable object are omitted whole, so
|
|
350
|
-
* their fields read as absent until a document supplies them. Must stay in
|
|
351
|
-
* lockstep with materializeDefaults in shared.ts. */
|
|
352
|
-
type Filled<S> = S extends {default: unknown}
|
|
353
|
-
? true
|
|
354
|
-
: S extends OptionalSchema
|
|
355
|
-
? false
|
|
356
|
-
: S extends ObjectSchema<infer Shape>
|
|
357
|
-
? ObjectSchema extends S
|
|
358
|
-
? false
|
|
359
|
-
: AllFilled<Shape>
|
|
360
|
-
: false
|
|
361
|
-
|
|
362
|
-
/* Every field fills or is optional; an empty shape fills as {}. */
|
|
363
|
-
type AllFilled<Shape> = false extends {
|
|
364
|
-
[K in keyof Shape]: Shape[K] extends OptionalSchema ? true : Filled<Shape[K]>
|
|
365
|
-
}[keyof Shape]
|
|
366
|
-
? false
|
|
367
|
-
: true
|
|
368
|
-
|
|
369
|
-
type InferReadObject<Shape> = {
|
|
370
|
-
[K in keyof Shape as Filled<Shape[K]> extends true ? K : never]: InferRead<Shape[K]>
|
|
371
|
-
} & {
|
|
372
|
-
[K in keyof Shape as Filled<Shape[K]> extends true ? never : K]?: InferRead<Shape[K]>
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ── Schema constructors ─────────────────────────────────────────────
|
|
376
|
-
|
|
377
|
-
/* Display annotations every constructor accepts. Structural arguments stay
|
|
378
|
-
* positional; annotations trail. */
|
|
379
|
-
export interface DisplayOptions {
|
|
380
|
-
readonly title?: string
|
|
381
|
-
readonly description?: string
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export interface ScalarOptions<T> extends DisplayOptions {
|
|
385
|
-
readonly default?: T
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export interface DefaultOption<T> extends DisplayOptions {
|
|
389
|
-
readonly default?: T
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/* `mask` says: do not display this value in cleartext. A form renders a
|
|
393
|
-
* password input, and any other consumer that prints a config document
|
|
394
|
-
* redacts. It is a display rule and nothing more: the value is stored,
|
|
395
|
-
* transmitted and held on the device in plaintext exactly as any other. */
|
|
396
|
-
export interface MaskableOptions<T> extends ScalarOptions<T> {
|
|
397
|
-
readonly mask?: boolean
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* Constraints, unlike the display annotations, change what validates. A
|
|
401
|
-
* consumer may ignore an annotation it does not recognise; it may not ignore
|
|
402
|
-
* one of these, since doing so means accepting a value the author ruled out. */
|
|
403
|
-
export interface StringOptions<T> extends MaskableOptions<T> {
|
|
404
|
-
readonly minLength?: number
|
|
405
|
-
readonly maxLength?: number
|
|
406
|
-
readonly format?: Format
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export interface NumberOptions<T> extends MaskableOptions<T> {
|
|
410
|
-
readonly min?: number
|
|
411
|
-
readonly max?: number
|
|
412
|
-
readonly integer?: boolean
|
|
413
|
-
readonly unit?: Unit
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export interface ArrayOptions<T> extends DefaultOption<T> {
|
|
417
|
-
readonly minItems?: number
|
|
418
|
-
readonly maxItems?: number
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/* A node interface types `default` as optional, so a defaulted node and a bare
|
|
422
|
-
* one are the same type; the constructors record the annotation in their
|
|
423
|
-
* return type instead, which is what lets InferRead see it. D is the inferred
|
|
424
|
-
* type of the `default` option, undefined when none was written. */
|
|
425
|
-
type Defaulted<S, D> = [D] extends [undefined] ? S : S & {readonly default: unknown}
|
|
426
|
-
|
|
427
|
-
/* Defaults below a wholesale unit never fill: applyDefaults replaces the unit
|
|
428
|
-
* whole, so only a unit-level default applies. Rejected where they are written
|
|
429
|
-
* rather than at the validation that later misses the field. The walk stops at
|
|
430
|
-
* a nested unit's own default, since that unit's constructor already cleared
|
|
431
|
-
* everything under it. */
|
|
432
|
-
function rejectInnerDefaults(node: Schema, path: string, unit: string, self: string): void {
|
|
433
|
-
if ((node as {default?: unknown}).default !== undefined) {
|
|
434
|
-
throw new TypeError(
|
|
435
|
-
`a default under ${unit} never applies; give ${self} itself a whole-value ` +
|
|
436
|
-
`default instead (found at ${path})`,
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
if (node.kind === 'object') {
|
|
440
|
-
const keys = Object.keys(node.shape)
|
|
441
|
-
for (let i = 0; i < keys.length; i++) {
|
|
442
|
-
rejectInnerDefaults(node.shape[keys[i]!]!, `${path}.${keys[i]!}`, unit, self)
|
|
443
|
-
}
|
|
444
|
-
} else if (node.kind === 'optional') {
|
|
445
|
-
// optional() rejects an inner default itself, so this only reaches what it
|
|
446
|
-
// wraps without reporting the same node twice.
|
|
447
|
-
rejectInnerDefaults(node.inner, path, unit, self)
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/* Copies the annotations onto the node and rejects a `default` whose *shape*
|
|
452
|
-
* the node would not accept, so an obviously wrong default fails where it is
|
|
453
|
-
* written. Annotations live on the node so a schema serializes to JSON as-is.
|
|
454
|
-
*
|
|
455
|
-
* Constraints are deliberately not checked here, because validate() below does
|
|
456
|
-
* not carry them: see its comment. A default that breaks its own bound is
|
|
457
|
-
* caught by parseConfigSchema in shared.ts, which runs when the config is
|
|
458
|
-
* packed, moments after this. */
|
|
459
|
-
const ANNOTATION_KEYS = [
|
|
460
|
-
'title',
|
|
461
|
-
'description',
|
|
462
|
-
'mask',
|
|
463
|
-
'minLength',
|
|
464
|
-
'maxLength',
|
|
465
|
-
'min',
|
|
466
|
-
'max',
|
|
467
|
-
'integer',
|
|
468
|
-
'minItems',
|
|
469
|
-
'maxItems',
|
|
470
|
-
'format',
|
|
471
|
-
'unit',
|
|
472
|
-
] as const
|
|
473
|
-
|
|
474
|
-
/* Every annotation any constructor accepts. Interfaces have no index
|
|
475
|
-
* signature, so the copy below reads through a Record view of this. */
|
|
476
|
-
type AnyOptions = DisplayOptions &
|
|
477
|
-
Partial<
|
|
478
|
-
Record<'mask' | 'integer', boolean> &
|
|
479
|
-
Record<'minLength' | 'maxLength' | 'min' | 'max' | 'minItems' | 'maxItems', number> & {
|
|
480
|
-
default: unknown
|
|
481
|
-
}
|
|
482
|
-
>
|
|
483
|
-
|
|
484
|
-
function annotate<S extends Schema>(node: S, options?: AnyOptions): S {
|
|
485
|
-
if (options === undefined) return node
|
|
486
|
-
const out = node as unknown as Record<string, unknown>
|
|
487
|
-
const src = options as Record<string, unknown>
|
|
488
|
-
for (let i = 0; i < ANNOTATION_KEYS.length; i++) {
|
|
489
|
-
const key = ANNOTATION_KEYS[i]!
|
|
490
|
-
if (src[key] !== undefined) out[key] = src[key]
|
|
491
|
-
}
|
|
492
|
-
if (options.default !== undefined) {
|
|
493
|
-
out.default = options.default
|
|
494
|
-
const result = validate(node, options.default, '')
|
|
495
|
-
if (result !== null) {
|
|
496
|
-
throw new TypeError(`schema default does not match the schema: ${result.error.message}`)
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return node
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function string<D extends string | undefined = undefined>(
|
|
503
|
-
options?: StringOptions<D>,
|
|
504
|
-
): Defaulted<StringSchema, D> {
|
|
505
|
-
return annotate<StringSchema>({kind: 'string'}, options) as Defaulted<StringSchema, D>
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
export function number<D extends number | undefined = undefined>(
|
|
509
|
-
options?: NumberOptions<D>,
|
|
510
|
-
): Defaulted<NumberSchema, D> {
|
|
511
|
-
return annotate<NumberSchema>({kind: 'number'}, options) as Defaulted<NumberSchema, D>
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export function boolean<D extends boolean | undefined = undefined>(
|
|
515
|
-
options?: ScalarOptions<D>,
|
|
516
|
-
): Defaulted<BooleanSchema, D> {
|
|
517
|
-
return annotate<BooleanSchema>({kind: 'boolean'}, options) as Defaulted<BooleanSchema, D>
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
export function unknown(): UnknownSchema {
|
|
521
|
-
return {kind: 'unknown'}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export function literal<T extends Primitive, D extends T | undefined = undefined>(
|
|
525
|
-
value: T,
|
|
526
|
-
options?: ScalarOptions<D>,
|
|
527
|
-
): Defaulted<LiteralSchema<T>, D> {
|
|
528
|
-
return annotate<LiteralSchema<T>>({kind: 'literal', value}, options) as Defaulted<
|
|
529
|
-
LiteralSchema<T>,
|
|
530
|
-
D
|
|
531
|
-
>
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export function array<S extends Schema, D extends NoInfer<Infer<S>>[] | undefined = undefined>(
|
|
535
|
-
element: S,
|
|
536
|
-
options?: ArrayOptions<D>,
|
|
537
|
-
): Defaulted<ArraySchema<S>, D> {
|
|
538
|
-
rejectInnerDefaults(element, '[]', 'an array', 'the array')
|
|
539
|
-
return annotate<ArraySchema<S>>({kind: 'array', element}, options) as Defaulted<ArraySchema<S>, D>
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export function object<Shape extends Record<string, Schema>>(
|
|
543
|
-
shape: Shape,
|
|
544
|
-
options?: DefaultOption<never>,
|
|
545
|
-
): ObjectSchema<Shape> {
|
|
546
|
-
if (options?.default !== undefined) {
|
|
547
|
-
throw new TypeError(
|
|
548
|
-
"an object's defaults compose from its fields; declare defaults on the fields",
|
|
549
|
-
)
|
|
550
|
-
}
|
|
551
|
-
return annotate<ObjectSchema<Shape>>({kind: 'object', shape}, options)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export function tuple<
|
|
555
|
-
Elements extends readonly Schema[],
|
|
556
|
-
D extends NoInfer<Infer<TupleSchema<Elements>>> | undefined = undefined,
|
|
557
|
-
>(elements: [...Elements], options?: DefaultOption<D>): Defaulted<TupleSchema<Elements>, D> {
|
|
558
|
-
for (let i = 0; i < elements.length; i++) {
|
|
559
|
-
rejectInnerDefaults(elements[i]!, `[${i}]`, 'a tuple', 'the tuple')
|
|
560
|
-
}
|
|
561
|
-
return annotate<TupleSchema<Elements>>({kind: 'tuple', elements}, options) as Defaulted<
|
|
562
|
-
TupleSchema<Elements>,
|
|
563
|
-
D
|
|
564
|
-
>
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export function optional<S extends Schema>(inner: S): OptionalSchema<S> {
|
|
568
|
-
if ((inner as {default?: unknown}).default !== undefined) {
|
|
569
|
-
throw new TypeError('optional() cannot wrap a schema with a default')
|
|
570
|
-
}
|
|
571
|
-
return {kind: 'optional', inner}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
export function union<
|
|
575
|
-
Members extends readonly Schema[],
|
|
576
|
-
D extends NoInfer<Infer<UnionSchema<Members>>> | undefined = undefined,
|
|
577
|
-
>(members: [...Members], options?: DefaultOption<D>): Defaulted<UnionSchema<Members>, D> {
|
|
578
|
-
for (let i = 0; i < members.length; i++) {
|
|
579
|
-
rejectInnerDefaults(members[i]!, `[${i}]`, 'a union', 'the union')
|
|
580
|
-
}
|
|
581
|
-
return annotate<UnionSchema<Members>>({kind: 'union', members}, options) as Defaulted<
|
|
582
|
-
UnionSchema<Members>,
|
|
583
|
-
D
|
|
584
|
-
>
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/* A closed list of values with a label for each, which is what a form renders
|
|
588
|
-
* as a select or a radio group.
|
|
589
|
-
*
|
|
590
|
-
* Sugar, not a node kind: it builds a union of annotated literals, so nothing
|
|
591
|
-
* downstream has to learn about it. parseConfigSchema already rejects an empty
|
|
592
|
-
* union, and diffConfigSchemas already reports a removed member as requiring an
|
|
593
|
-
* operator, which is exactly what a dropped choice is.
|
|
594
|
-
*
|
|
595
|
-
* Use union([literal(...)]) directly when the values need no labels; labels are
|
|
596
|
-
* the whole point of this one. Named enumOf because `enum` is a reserved word:
|
|
597
|
-
* an export called `enum` could not be imported under its own name. */
|
|
598
|
-
export interface EnumEntry<T extends Primitive> {
|
|
599
|
-
readonly value: T
|
|
600
|
-
readonly title?: string
|
|
601
|
-
readonly description?: string
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
type EnumMembers<Entries extends readonly EnumEntry<Primitive>[]> = {
|
|
605
|
-
[K in keyof Entries]: LiteralSchema<Entries[K]['value']>
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
export function enumOf<
|
|
609
|
-
const Entries extends readonly EnumEntry<Primitive>[],
|
|
610
|
-
D extends Entries[number]['value'] | undefined = undefined,
|
|
611
|
-
>(entries: Entries, options?: DefaultOption<D>): Defaulted<UnionSchema<EnumMembers<Entries>>, D> {
|
|
612
|
-
const members = entries.map((entry) =>
|
|
613
|
-
literal(entry.value, {title: entry.title, description: entry.description}),
|
|
614
|
-
) as unknown as EnumMembers<Entries>
|
|
615
|
-
return annotate<UnionSchema<EnumMembers<Entries>>>(
|
|
616
|
-
{kind: 'union', members},
|
|
617
|
-
options,
|
|
618
|
-
) as Defaulted<UnionSchema<EnumMembers<Entries>>, D>
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
export function taggedUnion<
|
|
622
|
-
Key extends string,
|
|
623
|
-
Branches extends Record<string, ObjectSchema>,
|
|
624
|
-
D extends NoInfer<Infer<TaggedUnionSchema<Key, Branches>>> | undefined = undefined,
|
|
625
|
-
>(
|
|
626
|
-
key: Key,
|
|
627
|
-
branches: Branches,
|
|
628
|
-
options?: DefaultOption<D>,
|
|
629
|
-
): Defaulted<TaggedUnionSchema<Key, Branches>, D> {
|
|
630
|
-
const tags = Object.keys(branches)
|
|
631
|
-
for (let i = 0; i < tags.length; i++) {
|
|
632
|
-
rejectInnerDefaults(branches[tags[i]!]!, `.${tags[i]!}`, 'a taggedUnion', 'the union')
|
|
633
|
-
}
|
|
634
|
-
return annotate<TaggedUnionSchema<Key, Branches>>(
|
|
635
|
-
{kind: 'taggedUnion', key, branches},
|
|
636
|
-
options,
|
|
637
|
-
) as Defaulted<TaggedUnionSchema<Key, Branches>, D>
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// ── Defaults ────────────────────────────────────────────────────────
|
|
641
|
-
|
|
642
|
-
/* Builds the effective value: schema defaults with `value` layered over them.
|
|
643
|
-
* Objects are structure and always materialize (recursing per field, unknown
|
|
644
|
-
* keys dropped); every other node is replaced wholesale by a present value, so
|
|
645
|
-
* defaults inside array elements or union branches are form hints, not fills.
|
|
646
|
-
* The result is unvalidated — a missing required field stays missing for
|
|
647
|
-
* parse() to report. */
|
|
648
|
-
export function applyDefaults(schema: Schema, value: unknown): unknown {
|
|
649
|
-
switch (schema.kind) {
|
|
650
|
-
case 'object': {
|
|
651
|
-
// A present non-object stays as-is so validation rejects it; replacing
|
|
652
|
-
// it with {} here would make `{mqtt: 42}` validate clean and never
|
|
653
|
-
// record a configError.
|
|
654
|
-
if (
|
|
655
|
-
value !== undefined &&
|
|
656
|
-
(typeof value !== 'object' || value === null || Array.isArray(value))
|
|
657
|
-
) {
|
|
658
|
-
return value
|
|
659
|
-
}
|
|
660
|
-
const src = value === undefined ? {} : (value as Record<string, unknown>)
|
|
661
|
-
const out: Record<string, unknown> = {}
|
|
662
|
-
const keys = Object.keys(schema.shape)
|
|
663
|
-
for (let i = 0; i < keys.length; i++) {
|
|
664
|
-
const key = keys[i]!
|
|
665
|
-
const field = schema.shape[key]!
|
|
666
|
-
// hasOwn, not indexing: a shape key like "constructor" must read as
|
|
667
|
-
// absent, not as the inherited prototype member.
|
|
668
|
-
const raw = Object.hasOwn(src, key) ? src[key] : undefined
|
|
669
|
-
if (field.kind === 'optional') {
|
|
670
|
-
if (raw !== undefined) out[key] = raw
|
|
671
|
-
} else {
|
|
672
|
-
const child = applyDefaults(field, raw)
|
|
673
|
-
if (child !== undefined) out[key] = child
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return out
|
|
677
|
-
}
|
|
678
|
-
case 'array':
|
|
679
|
-
if (value !== undefined) return value
|
|
680
|
-
return schema.default !== undefined ? schema.default : []
|
|
681
|
-
case 'unknown':
|
|
682
|
-
case 'optional':
|
|
683
|
-
return value
|
|
684
|
-
default:
|
|
685
|
-
return value !== undefined ? value : (schema as {default?: unknown}).default
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// ── Parse ───────────────────────────────────────────────────────────
|
|
690
|
-
|
|
691
|
-
function typeOf(value: unknown): string {
|
|
692
|
-
if (value === null) return 'null'
|
|
693
|
-
if (Array.isArray(value)) return 'array'
|
|
694
|
-
return typeof value
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/* Structure only: the shape of a value, never a constraint on it.
|
|
698
|
-
*
|
|
699
|
-
* Constraints (min, max, integer, minLength, maxLength, minItems, maxItems,
|
|
700
|
-
* format) are enforced host-side in shared.ts, not here. This module is bundled
|
|
701
|
-
* into the device, and a config schema never reaches a device: it is validated
|
|
702
|
-
* where the registry runs and where the CLI packs. Carrying the checks here
|
|
703
|
-
* charged every app that imports mikro/schema for enforcement it could not use,
|
|
704
|
-
* measured at about 4 KB of heap on the `+ schema` bench checkpoint, most of it
|
|
705
|
-
* the format expressions.
|
|
706
|
-
*
|
|
707
|
-
* The cost of that split, stated plainly: parse() on the device checks that a
|
|
708
|
-
* number is a number, not that it is within its declared bounds. Constraints in
|
|
709
|
-
* a schema are a config-authoring feature. */
|
|
710
|
-
export function validate(
|
|
711
|
-
schema: Schema,
|
|
712
|
-
value: unknown,
|
|
713
|
-
path: string,
|
|
714
|
-
): ReturnType<typeof err<SchemaError>> | null {
|
|
715
|
-
switch (schema.kind) {
|
|
716
|
-
case 'string':
|
|
717
|
-
if (typeof value !== 'string')
|
|
718
|
-
return err(SchemaError.ValidationFailed(`expected string, got ${typeOf(value)}`, path))
|
|
719
|
-
return null
|
|
720
|
-
|
|
721
|
-
case 'number':
|
|
722
|
-
if (typeof value !== 'number' || Number.isNaN(value))
|
|
723
|
-
return err(SchemaError.ValidationFailed(`expected number, got ${typeOf(value)}`, path))
|
|
724
|
-
return null
|
|
725
|
-
|
|
726
|
-
case 'boolean':
|
|
727
|
-
if (typeof value !== 'boolean')
|
|
728
|
-
return err(SchemaError.ValidationFailed(`expected boolean, got ${typeOf(value)}`, path))
|
|
729
|
-
return null
|
|
730
|
-
|
|
731
|
-
case 'unknown':
|
|
732
|
-
return null
|
|
733
|
-
|
|
734
|
-
case 'literal':
|
|
735
|
-
if (value !== schema.value)
|
|
736
|
-
return err(
|
|
737
|
-
SchemaError.ValidationFailed(
|
|
738
|
-
`expected ${JSON.stringify(schema.value)}, got ${JSON.stringify(value)}`,
|
|
739
|
-
path,
|
|
740
|
-
),
|
|
741
|
-
)
|
|
742
|
-
return null
|
|
743
|
-
|
|
744
|
-
case 'array': {
|
|
745
|
-
if (!Array.isArray(value))
|
|
746
|
-
return err(SchemaError.ValidationFailed(`expected array, got ${typeOf(value)}`, path))
|
|
747
|
-
for (let i = 0; i < value.length; i++) {
|
|
748
|
-
const result = validate(schema.element, value[i], `${path}[${i}]`)
|
|
749
|
-
if (result !== null) return result
|
|
750
|
-
}
|
|
751
|
-
return null
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
case 'object': {
|
|
755
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
756
|
-
return err(SchemaError.ValidationFailed(`expected object, got ${typeOf(value)}`, path))
|
|
757
|
-
const obj = value as Record<string, unknown>
|
|
758
|
-
const keys = Object.keys(schema.shape)
|
|
759
|
-
for (let i = 0; i < keys.length; i++) {
|
|
760
|
-
const key = keys[i]!
|
|
761
|
-
const fieldSchema = schema.shape[key]!
|
|
762
|
-
const fieldPath = `${path}.${key}`
|
|
763
|
-
// hasOwn, like the taggedUnion dispatch: an inherited "constructor"
|
|
764
|
-
// must not stand in for a field.
|
|
765
|
-
if (fieldSchema.kind === 'optional') {
|
|
766
|
-
if (Object.hasOwn(obj, key)) {
|
|
767
|
-
const result = validate(fieldSchema, obj[key], fieldPath)
|
|
768
|
-
if (result !== null) return result
|
|
769
|
-
}
|
|
770
|
-
} else {
|
|
771
|
-
if (!Object.hasOwn(obj, key))
|
|
772
|
-
return err(SchemaError.ValidationFailed(`missing required field`, fieldPath))
|
|
773
|
-
const result = validate(fieldSchema, obj[key], fieldPath)
|
|
774
|
-
if (result !== null) return result
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
return null
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
case 'tuple': {
|
|
781
|
-
if (!Array.isArray(value))
|
|
782
|
-
return err(SchemaError.ValidationFailed(`expected array, got ${typeOf(value)}`, path))
|
|
783
|
-
if (value.length !== schema.elements.length)
|
|
784
|
-
return err(
|
|
785
|
-
SchemaError.ValidationFailed(
|
|
786
|
-
`expected ${schema.elements.length} elements, got ${value.length}`,
|
|
787
|
-
path,
|
|
788
|
-
),
|
|
789
|
-
)
|
|
790
|
-
for (let i = 0; i < schema.elements.length; i++) {
|
|
791
|
-
const result = validate(schema.elements[i]!, value[i], `${path}[${i}]`)
|
|
792
|
-
if (result !== null) return result
|
|
793
|
-
}
|
|
794
|
-
return null
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
case 'optional': {
|
|
798
|
-
if (value === undefined) return null
|
|
799
|
-
return validate(schema.inner, value, path)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
case 'union': {
|
|
803
|
-
for (let i = 0; i < schema.members.length; i++) {
|
|
804
|
-
const result = validate(schema.members[i]!, value, path)
|
|
805
|
-
if (result === null) return null
|
|
806
|
-
}
|
|
807
|
-
return err(SchemaError.ValidationFailed(`value did not match any union member`, path))
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
case 'taggedUnion': {
|
|
811
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
812
|
-
return err(SchemaError.ValidationFailed(`expected object, got ${typeOf(value)}`, path))
|
|
813
|
-
const obj = value as Record<string, unknown>
|
|
814
|
-
const tag = obj[schema.key]
|
|
815
|
-
if (tag === undefined)
|
|
816
|
-
return err(
|
|
817
|
-
SchemaError.ValidationFailed(`missing discriminator field`, `${path}.${schema.key}`),
|
|
818
|
-
)
|
|
819
|
-
if (typeof tag !== 'string' && typeof tag !== 'number' && typeof tag !== 'boolean')
|
|
820
|
-
return err(
|
|
821
|
-
SchemaError.ValidationFailed(
|
|
822
|
-
`expected primitive discriminator, got ${typeOf(tag)}`,
|
|
823
|
-
`${path}.${schema.key}`,
|
|
824
|
-
),
|
|
825
|
-
)
|
|
826
|
-
// hasOwn, not indexing: a tag like "constructor" must not resolve to
|
|
827
|
-
// an inherited property and validate against garbage.
|
|
828
|
-
const branch = Object.hasOwn(schema.branches, tag as string)
|
|
829
|
-
? schema.branches[tag as string]
|
|
830
|
-
: undefined
|
|
831
|
-
if (branch === undefined)
|
|
832
|
-
return err(
|
|
833
|
-
SchemaError.ValidationFailed(
|
|
834
|
-
`unknown tag ${JSON.stringify(tag)}`,
|
|
835
|
-
`${path}.${schema.key}`,
|
|
836
|
-
),
|
|
837
|
-
)
|
|
838
|
-
return validate(branch, value, path)
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
return err(SchemaError.ValidationFailed(`unknown schema kind: ${(schema as any).kind}`, path))
|
|
842
|
-
}
|