@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/CMakeLists.txt
CHANGED
|
@@ -43,7 +43,10 @@ set(MIKROJS_CORE_SOURCES
|
|
|
43
43
|
src/mik_text_encoding.cpp
|
|
44
44
|
src/mik_color.cpp
|
|
45
45
|
src/mik_abort.cpp
|
|
46
|
+
src/mik_http_client.cpp
|
|
47
|
+
src/mik_wifi_client.cpp
|
|
46
48
|
src/mik_cbor.cpp
|
|
49
|
+
src/mik_schema.cpp
|
|
47
50
|
src/mik_result.cpp
|
|
48
51
|
src/mik_console.cpp
|
|
49
52
|
src/mik_sys.cpp
|
|
@@ -88,7 +91,7 @@ endif()
|
|
|
88
91
|
include(cmake/mikrojs_bytecode.cmake)
|
|
89
92
|
mikrojs_generate_bytecode(
|
|
90
93
|
RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
|
|
91
|
-
MODULES abort cbor env result schema fs
|
|
94
|
+
MODULES abort cbor env result schema fs i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators ota ota/client ota/config pin pwm reader sleep spi sntp stdio stream sys test uart udp
|
|
92
95
|
MODULE_PREFIX "mikro"
|
|
93
96
|
SYMBOL_PREFIX "mikro"
|
|
94
97
|
TARGET gen_bytecode
|
|
@@ -208,6 +211,8 @@ if(BUILD_TESTING)
|
|
|
208
211
|
test/stream_test.cpp
|
|
209
212
|
test/runtime_recycle_test.cpp
|
|
210
213
|
test/udp_test.cpp
|
|
214
|
+
test/http_client_test.cpp
|
|
215
|
+
test/wifi_client_test.cpp
|
|
211
216
|
test/observable_test.cpp
|
|
212
217
|
test/unhandled_rejection_test.cpp
|
|
213
218
|
test/ota_policy_test.cpp
|
|
@@ -215,6 +220,7 @@ if(BUILD_TESTING)
|
|
|
215
220
|
test/ota_config_test.cpp
|
|
216
221
|
test/ota_js_hooks_test.cpp
|
|
217
222
|
test/ota_wire_fixtures_test.cpp
|
|
223
|
+
test/schema_conformance_test.cpp
|
|
218
224
|
test/sys_codec_test.cpp
|
|
219
225
|
)
|
|
220
226
|
|
|
@@ -234,9 +240,25 @@ if(BUILD_TESTING)
|
|
|
234
240
|
)
|
|
235
241
|
add_dependencies(mikrojs_tests gen_checkin_fixtures)
|
|
236
242
|
|
|
243
|
+
# ── Schema conformance fixtures ──────────────────────────────────
|
|
244
|
+
# mikro/schema has two implementations: @mikrojs/schema (host) and
|
|
245
|
+
# src/mik_schema.cpp (device). The generator records what core.ts does and
|
|
246
|
+
# test/schema_conformance_test.cpp replays it through the native module.
|
|
247
|
+
# Always regenerated so the fixtures cannot lag core.ts.
|
|
248
|
+
set(_SCHEMA_FIXTURE_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen/schema-fixtures")
|
|
249
|
+
add_custom_target(gen_schema_fixtures
|
|
250
|
+
COMMAND node "${CMAKE_CURRENT_SOURCE_DIR}/scripts/gen-schema-fixtures.js"
|
|
251
|
+
"${_SCHEMA_FIXTURE_DIR}"
|
|
252
|
+
COMMENT "Generating schema conformance fixtures from @mikrojs/schema"
|
|
253
|
+
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
254
|
+
VERBATIM
|
|
255
|
+
)
|
|
256
|
+
add_dependencies(mikrojs_tests gen_schema_fixtures)
|
|
257
|
+
|
|
237
258
|
target_link_libraries(mikrojs_tests PRIVATE mikrojs)
|
|
238
259
|
target_compile_definitions(mikrojs_tests PRIVATE
|
|
239
|
-
"MIK_CHECKIN_FIXTURE_DIR=\"${_CHECKIN_FIXTURE_DIR}\""
|
|
260
|
+
"MIK_CHECKIN_FIXTURE_DIR=\"${_CHECKIN_FIXTURE_DIR}\""
|
|
261
|
+
"MIK_SCHEMA_FIXTURE_DIR=\"${_SCHEMA_FIXTURE_DIR}\"")
|
|
240
262
|
target_include_directories(mikrojs_tests PRIVATE
|
|
241
263
|
"${CMAKE_CURRENT_SOURCE_DIR}/test"
|
|
242
264
|
"${CMAKE_CURRENT_SOURCE_DIR}/deps/nanocbor/include"
|
|
@@ -115,6 +115,10 @@ struct MIKRuntime {
|
|
|
115
115
|
* mik__result_ok_void. Safe to share because Result is immutable by
|
|
116
116
|
* convention, and the singleton is frozen + non-extensible. */
|
|
117
117
|
JSValue result_ok_void_singleton;
|
|
118
|
+
/* Shared BodyConsumedError constructor for the native mikro/http/helpers
|
|
119
|
+
* module. Created lazily on first http module init, freed in
|
|
120
|
+
* MIK_FreeRuntime. */
|
|
121
|
+
JSValue http_body_consumed_ctor;
|
|
118
122
|
JSValue env_obj; /* Frozen object with env vars, set on import.meta.env */
|
|
119
123
|
struct {
|
|
120
124
|
JSValue on_data; /* JS callback for stdin data, JS_UNDEFINED when not listening */
|
|
@@ -255,9 +259,33 @@ std::string mik_inspect(JSContext* ctx, JSValue value, int depth = 2, bool color
|
|
|
255
259
|
bool show_hidden = false);
|
|
256
260
|
void mik__inspect_register(JSContext* ctx);
|
|
257
261
|
|
|
262
|
+
/* HTTP client modules mikro/http/helpers + mikro/http/request
|
|
263
|
+
* (mik_http_client.cpp). Loaded lazily via the C-module table in
|
|
264
|
+
* modules.cpp, not registered at runtime creation. */
|
|
265
|
+
JSModuleDef* mik__http_helpers_load(JSContext* ctx);
|
|
266
|
+
JSModuleDef* mik__http_request_load(JSContext* ctx);
|
|
267
|
+
|
|
268
|
+
/* Wifi client module mikro/wifi (mik_wifi_client.cpp), same lazy table. */
|
|
269
|
+
JSModuleDef* mik__wifi_client_load(JSContext* ctx);
|
|
270
|
+
|
|
271
|
+
/* Load `specifier` through the module loader on behalf of `importer` and
|
|
272
|
+
* return its namespace (modules.cpp). Honors virtual-module overrides. */
|
|
273
|
+
JSValue mik__load_module_ns(JSContext* ctx, const char* importer, const char* specifier,
|
|
274
|
+
bool require_evaluated);
|
|
275
|
+
|
|
276
|
+
/* Timer registry (timers.cpp) — direct scheduling for native callbacks that
|
|
277
|
+
* must not go through the JS setTimeout global. Schedule dups `func`. */
|
|
278
|
+
uint32_t MIK_Timer_Schedule(MIKTimers* timers, JSContext* ctx, JSValue func, int argc,
|
|
279
|
+
const JSValue* argv, int64_t timeout, bool is_interval, int64_t now);
|
|
280
|
+
bool MIK_Timer_UnSchedule(MIKTimers* timers, JSContext* ctx, uint32_t id);
|
|
281
|
+
|
|
258
282
|
/* CBOR module (mik_cbor.cpp) */
|
|
259
283
|
JSModuleDef* mik__cbor_init(JSContext* ctx);
|
|
260
284
|
|
|
285
|
+
/* Schema module (mik_schema.cpp). Registered lazily: the loader calls its
|
|
286
|
+
* init on first import of native:mikro/schema. */
|
|
287
|
+
void mik__schema_register(void);
|
|
288
|
+
|
|
261
289
|
/* Result module (mik_result.cpp) */
|
|
262
290
|
JSModuleDef* mik__result_init(JSContext* ctx);
|
|
263
291
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -70,8 +70,6 @@
|
|
|
70
70
|
"./runtime/reader/types": "./runtime/reader/types.ts",
|
|
71
71
|
"./runtime/result/native-result.node-shim": "./dist/runtime/result/native-result.node-shim.js",
|
|
72
72
|
"./runtime/result/types": "./runtime/result/types.ts",
|
|
73
|
-
"./runtime/schema/core": "./dist/runtime/schema/core.js",
|
|
74
|
-
"./runtime/schema/shared": "./dist/runtime/schema/shared.js",
|
|
75
73
|
"./runtime/schema/types": "./runtime/schema/types.ts",
|
|
76
74
|
"./runtime/sleep/types": "./runtime/sleep/types.ts",
|
|
77
75
|
"./runtime/sntp/types": "./runtime/sntp/types.ts",
|
|
@@ -88,14 +86,15 @@
|
|
|
88
86
|
"cmake-js": "^8.0.0",
|
|
89
87
|
"node-addon-api": "^8.7.0",
|
|
90
88
|
"node-gyp-build": "^4.8.4",
|
|
91
|
-
"@mikrojs/quickjs": "0.
|
|
89
|
+
"@mikrojs/quickjs": "0.19.0"
|
|
92
90
|
},
|
|
93
91
|
"devDependencies": {
|
|
94
92
|
"@swc/core": "^1.15.30",
|
|
95
93
|
"@types/node": "^24.12.2",
|
|
96
94
|
"esbuild": "^0.28.0",
|
|
97
95
|
"terser": "^5.46.2",
|
|
98
|
-
"@mikrojs/registry": "0.
|
|
96
|
+
"@mikrojs/registry": "0.19.0",
|
|
97
|
+
"@mikrojs/schema": "0.19.0"
|
|
99
98
|
},
|
|
100
99
|
"engines": {
|
|
101
100
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/http/helpers.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// `native:mikro/http`. Apps driving HTTP through a non-lwIP transport (e.g. an
|
|
3
3
|
// LTE modem over UART) implement the `Request` type directly and reuse
|
|
4
4
|
// `prepareBody` + `makeResponse` for the boring parts.
|
|
5
|
+
//
|
|
6
|
+
// On-device, `mikro/http/helpers` is the native C module in
|
|
7
|
+
// src/mik_http_client.cpp (behavior pinned by test/http_client_test.cpp).
|
|
8
|
+
// This TS source stays as the type surface and as the Node-side double for
|
|
9
|
+
// the http/server vitest suite.
|
|
5
10
|
|
|
6
11
|
import {err, ok} from 'mikro/result'
|
|
7
12
|
|
package/runtime/internal.d.ts
CHANGED
|
@@ -26,6 +26,34 @@ declare module 'native:mikro/cbor' {
|
|
|
26
26
|
export function decode(data: Uint8Array): Result<unknown, CborError>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
declare module 'native:mikro/schema' {
|
|
30
|
+
import type * as S from '@mikrojs/native/runtime/schema/types'
|
|
31
|
+
/* typeof-aliased rather than re-exported: `export ... from` inside an
|
|
32
|
+
* ambient module declaration resolves to never, and writing the generic
|
|
33
|
+
* signatures out again here would be a third copy of the surface. */
|
|
34
|
+
export const string: typeof S.string
|
|
35
|
+
export const number: typeof S.number
|
|
36
|
+
export const boolean: typeof S.boolean
|
|
37
|
+
export const unknown: typeof S.unknown
|
|
38
|
+
export const literal: typeof S.literal
|
|
39
|
+
export const array: typeof S.array
|
|
40
|
+
export const object: typeof S.object
|
|
41
|
+
export const tuple: typeof S.tuple
|
|
42
|
+
export const optional: typeof S.optional
|
|
43
|
+
export const union: typeof S.union
|
|
44
|
+
export const enumOf: typeof S.enumOf
|
|
45
|
+
export const taggedUnion: typeof S.taggedUnion
|
|
46
|
+
export const applyDefaults: typeof S.applyDefaults
|
|
47
|
+
export const SchemaError: typeof S.SchemaError
|
|
48
|
+
/* The plain {ok, error} envelope core.ts's local err() builds, not a
|
|
49
|
+
* mikro/result Result: parse() in schema.ts wraps it. */
|
|
50
|
+
export function validate(
|
|
51
|
+
schema: S.Schema,
|
|
52
|
+
value: unknown,
|
|
53
|
+
path: string,
|
|
54
|
+
): {ok: false; error: S.SchemaError} | null
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
declare module 'native:mikro/result' {
|
|
30
58
|
import type {ErrResult, OkResult} from './result/types.js'
|
|
31
59
|
export function ok(): OkResult<void>
|
package/runtime/ota/types.ts
CHANGED
|
@@ -374,9 +374,9 @@ export interface Ota {
|
|
|
374
374
|
* `applyConfig`, with `trialBoots`), and validate the offer fields (exactly
|
|
375
375
|
* `parseOffer(raw)`, with `allowInsecure`). An empty or null response is
|
|
376
376
|
* the registry's quiet round: nothing to deliver, and the confirm still
|
|
377
|
-
* happens, which is the point of calling. A response
|
|
378
|
-
*
|
|
379
|
-
* error page
|
|
377
|
+
* happens, which is the point of calling. A response of any other shape
|
|
378
|
+
* never decoded: a captive portal handed back HTML, or a proxy sent an
|
|
379
|
+
* error page. That is not a completed round, so nothing settles and the
|
|
380
380
|
* confirm does not run.
|
|
381
381
|
*
|
|
382
382
|
* Never call it on a failed request: a check-in that did not complete
|
package/runtime/schema/schema.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
/* mikro/schema. The constructors, the validator and applyDefaults are native
|
|
2
|
+
* (src/mik_schema.cpp); all this adds is the Result-returning parse(), which
|
|
3
|
+
* is the only part that needs mikro/result.
|
|
4
|
+
*
|
|
5
|
+
* @mikrojs/schema is the same DSL in TypeScript and is what the host
|
|
6
|
+
* runs (the CLI evaluating mikro.config.ts, the registry, vitest). The two are
|
|
7
|
+
* held together by scripts/gen-schema-fixtures.js and
|
|
8
|
+
* test/schema_conformance_test.cpp — change core.ts first, then mik_schema.cpp.
|
|
9
|
+
*
|
|
10
|
+
* validate() is deliberately not re-exported: parse() is the public entry, and
|
|
11
|
+
* that has been true since this module was bytecode. */
|
|
12
|
+
|
|
1
13
|
import {err, ok} from 'mikro/result'
|
|
14
|
+
import {validate} from 'native:mikro/schema'
|
|
2
15
|
|
|
3
16
|
import type {Result} from '../result/types.js'
|
|
4
|
-
import
|
|
17
|
+
import type {Infer, Schema, SchemaError} from './types.js'
|
|
5
18
|
|
|
6
19
|
export type {
|
|
7
20
|
ArrayOptions,
|
|
@@ -28,7 +41,7 @@ export type {
|
|
|
28
41
|
UnionSchema,
|
|
29
42
|
Unit,
|
|
30
43
|
UnknownSchema,
|
|
31
|
-
} from './
|
|
44
|
+
} from './types.js'
|
|
32
45
|
export {
|
|
33
46
|
applyDefaults,
|
|
34
47
|
array,
|
|
@@ -44,7 +57,7 @@ export {
|
|
|
44
57
|
tuple,
|
|
45
58
|
union,
|
|
46
59
|
unknown,
|
|
47
|
-
} from '
|
|
60
|
+
} from 'native:mikro/schema'
|
|
48
61
|
|
|
49
62
|
export function parse<S extends Schema>(schema: S, value: unknown): Result<Infer<S>, SchemaError> {
|
|
50
63
|
const result = validate(schema, value, '')
|
package/runtime/schema/types.ts
CHANGED
|
@@ -281,7 +281,7 @@ export type InferRead<S> =
|
|
|
281
281
|
: Simplify<InferReadObject<Shape>>
|
|
282
282
|
: Infer<S>
|
|
283
283
|
|
|
284
|
-
/* Kept in lockstep with
|
|
284
|
+
/* Kept in lockstep with @mikrojs/schema (and its materializeDefaults). */
|
|
285
285
|
type Filled<S> = S extends {default: unknown}
|
|
286
286
|
? true
|
|
287
287
|
: S extends OptionalSchema
|
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')}`)
|
package/src/mik_app_config.cpp
CHANGED
|
@@ -337,7 +337,15 @@ int MIK_LoadConfig(const char* base_path, MIKConfig* config) {
|
|
|
337
337
|
config->stack_size = (size_t)num_val;
|
|
338
338
|
}
|
|
339
339
|
if (mik__json_get_number(buf, "memReserved", &num_val)) {
|
|
340
|
-
|
|
340
|
+
/* The uint32 cast would wrap a negative value into a ~4 GB
|
|
341
|
+
* reserve; keep the default instead. */
|
|
342
|
+
if (num_val < 0) {
|
|
343
|
+
platform->log(MIK_LOG_WARN, TAG,
|
|
344
|
+
"Ignoring negative memReserved (%ld); using default",
|
|
345
|
+
(long)num_val);
|
|
346
|
+
} else {
|
|
347
|
+
config->mem_reserved = (uint32_t)num_val;
|
|
348
|
+
}
|
|
341
349
|
}
|
|
342
350
|
if (mik__json_get_number(buf, "fsReadMax", &num_val)) {
|
|
343
351
|
config->fs_read_max = (uint32_t)num_val;
|