@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
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
/* native:mikro/schema — the device half of the schema DSL.
|
|
2
|
+
*
|
|
3
|
+
* A faithful port of @mikrojs/schema, which stays as the host
|
|
4
|
+
* implementation (the CLI evaluating mikro.config.ts, the registry, vitest).
|
|
5
|
+
* Two implementations of one contract, so they are checked against a generated
|
|
6
|
+
* corpus rather than two independent suites: scripts/gen-schema-fixtures.js
|
|
7
|
+
* records what core.ts does and test/schema_conformance_test.cpp replays it
|
|
8
|
+
* here. Change behaviour in core.ts first, then here, and let the corpus say
|
|
9
|
+
* whether they still agree.
|
|
10
|
+
*
|
|
11
|
+
* validate() covers shape and every numeric and length bound. `format` and
|
|
12
|
+
* `unit` stay host-side in @mikrojs/schema/config: format needs expressions there is
|
|
13
|
+
* no engine for here, and unit is a display hint that constrains nothing.
|
|
14
|
+
*
|
|
15
|
+
* parse() is not here: it wraps validate() in a Result and lives in the JS
|
|
16
|
+
* facade, which keeps this module free of any Result coupling. */
|
|
17
|
+
|
|
18
|
+
#include <cmath>
|
|
19
|
+
#include <cstdio>
|
|
20
|
+
#include <cstring>
|
|
21
|
+
#include <string>
|
|
22
|
+
|
|
23
|
+
#include <quickjs.h>
|
|
24
|
+
|
|
25
|
+
#include "mikrojs/mikrojs.h"
|
|
26
|
+
#include "mikrojs/utils.h"
|
|
27
|
+
|
|
28
|
+
namespace {
|
|
29
|
+
|
|
30
|
+
/* ── Node kinds ─────────────────────────────────────────────────────── */
|
|
31
|
+
|
|
32
|
+
enum class Kind {
|
|
33
|
+
String,
|
|
34
|
+
Number,
|
|
35
|
+
Boolean,
|
|
36
|
+
Unknown,
|
|
37
|
+
Literal,
|
|
38
|
+
Array,
|
|
39
|
+
Object,
|
|
40
|
+
Optional,
|
|
41
|
+
Tuple,
|
|
42
|
+
Union,
|
|
43
|
+
TaggedUnion,
|
|
44
|
+
Invalid,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
Kind ParseKind(const char* k) {
|
|
48
|
+
if (!k) return Kind::Invalid;
|
|
49
|
+
if (!strcmp(k, "string")) return Kind::String;
|
|
50
|
+
if (!strcmp(k, "number")) return Kind::Number;
|
|
51
|
+
if (!strcmp(k, "boolean")) return Kind::Boolean;
|
|
52
|
+
if (!strcmp(k, "unknown")) return Kind::Unknown;
|
|
53
|
+
if (!strcmp(k, "literal")) return Kind::Literal;
|
|
54
|
+
if (!strcmp(k, "array")) return Kind::Array;
|
|
55
|
+
if (!strcmp(k, "object")) return Kind::Object;
|
|
56
|
+
if (!strcmp(k, "optional")) return Kind::Optional;
|
|
57
|
+
if (!strcmp(k, "tuple")) return Kind::Tuple;
|
|
58
|
+
if (!strcmp(k, "union")) return Kind::Union;
|
|
59
|
+
if (!strcmp(k, "taggedUnion")) return Kind::TaggedUnion;
|
|
60
|
+
return Kind::Invalid;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Reads `kind` and keeps the raw text, which the fallthrough error reports. */
|
|
64
|
+
struct NodeKind {
|
|
65
|
+
Kind kind = Kind::Invalid;
|
|
66
|
+
std::string raw;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
NodeKind KindOf(JSContext* ctx, JSValueConst schema) {
|
|
70
|
+
NodeKind out;
|
|
71
|
+
JSValue kv = JS_GetPropertyStr(ctx, schema, "kind");
|
|
72
|
+
const char* text = JS_ToCString(ctx, kv);
|
|
73
|
+
if (text) {
|
|
74
|
+
out.raw = text;
|
|
75
|
+
out.kind = ParseKind(text);
|
|
76
|
+
JS_FreeCString(ctx, text);
|
|
77
|
+
}
|
|
78
|
+
JS_FreeValue(ctx, kv);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── Own-property access ────────────────────────────────────────────── */
|
|
83
|
+
|
|
84
|
+
/* Object.hasOwn. JS_GetPropertyStr walks the prototype chain, which would let
|
|
85
|
+
* an inherited member stand in for a required field or a union branch — a
|
|
86
|
+
* value of {} would satisfy object({constructor: unknown()}), and a tag of
|
|
87
|
+
* "constructor" would dispatch to Object.prototype.constructor. core.ts guards
|
|
88
|
+
* the same three sites for the same reason. */
|
|
89
|
+
bool HasOwn(JSContext* ctx, JSValueConst obj, JSAtom key) {
|
|
90
|
+
JSPropertyDescriptor desc;
|
|
91
|
+
int found = JS_GetOwnProperty(ctx, &desc, obj, key);
|
|
92
|
+
if (found != 1) return false;
|
|
93
|
+
JS_FreeValue(ctx, desc.value);
|
|
94
|
+
JS_FreeValue(ctx, desc.getter);
|
|
95
|
+
JS_FreeValue(ctx, desc.setter);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Own enumerable string keys, in insertion order — Object.keys(). */
|
|
100
|
+
struct OwnKeys {
|
|
101
|
+
JSPropertyEnum* tab = nullptr;
|
|
102
|
+
uint32_t len = 0;
|
|
103
|
+
JSContext* ctx = nullptr;
|
|
104
|
+
|
|
105
|
+
OwnKeys(JSContext* c, JSValueConst obj) : ctx(c) {
|
|
106
|
+
if (JS_GetOwnPropertyNames(c, &tab, &len, obj, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) !=
|
|
107
|
+
0) {
|
|
108
|
+
tab = nullptr;
|
|
109
|
+
len = 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
~OwnKeys() {
|
|
113
|
+
if (tab) JS_FreePropertyEnum(ctx, tab, len);
|
|
114
|
+
}
|
|
115
|
+
OwnKeys(const OwnKeys&) = delete;
|
|
116
|
+
OwnKeys& operator=(const OwnKeys&) = delete;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/* ── typeOf() ───────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const char* TypeOf(JSContext* ctx, JSValueConst value) {
|
|
122
|
+
if (JS_IsNull(value)) return "null";
|
|
123
|
+
if (JS_IsArray(value)) return "array";
|
|
124
|
+
if (JS_IsUndefined(value)) return "undefined";
|
|
125
|
+
if (JS_IsBool(value)) return "boolean";
|
|
126
|
+
if (JS_IsNumber(value)) return "number";
|
|
127
|
+
if (JS_IsString(value)) return "string";
|
|
128
|
+
if (JS_IsSymbol(value)) return "symbol";
|
|
129
|
+
if (JS_IsBigInt(value)) return "bigint";
|
|
130
|
+
if (JS_IsFunction(ctx, value)) return "function";
|
|
131
|
+
return "object";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* JSON.stringify, as the literal messages use it. Undefined and functions
|
|
135
|
+
* stringify to nothing, which a template literal renders as "undefined". */
|
|
136
|
+
std::string JsonText(JSContext* ctx, JSValueConst value) {
|
|
137
|
+
JSValue json = JS_JSONStringify(ctx, value, JS_UNDEFINED, JS_UNDEFINED);
|
|
138
|
+
if (JS_IsException(json)) {
|
|
139
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
140
|
+
return "undefined";
|
|
141
|
+
}
|
|
142
|
+
const char* text = JS_ToCString(ctx, json);
|
|
143
|
+
std::string out = text ? text : "undefined";
|
|
144
|
+
if (text) JS_FreeCString(ctx, text);
|
|
145
|
+
JS_FreeValue(ctx, json);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Validation ─────────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
/* An absent or non-numeric annotation means unconstrained, matching the
|
|
152
|
+
* `!== undefined` guards in core.ts. */
|
|
153
|
+
bool NumberAnnotation(JSContext* ctx, JSValueConst schema, const char* key, double* out) {
|
|
154
|
+
JSValue v = JS_GetPropertyStr(ctx, schema, key);
|
|
155
|
+
bool present = JS_IsNumber(v) && JS_ToFloat64(ctx, out, v) == 0;
|
|
156
|
+
JS_FreeValue(ctx, v);
|
|
157
|
+
return present;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
bool IsTrue(JSContext* ctx, JSValueConst schema, const char* key) {
|
|
161
|
+
JSValue v = JS_GetPropertyStr(ctx, schema, key);
|
|
162
|
+
bool yes = JS_IsBool(v) && JS_ToBool(ctx, v) == 1;
|
|
163
|
+
JS_FreeValue(ctx, v);
|
|
164
|
+
return yes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* A number the way JS renders it in a template literal. Deferred to the engine
|
|
168
|
+
* rather than reimplemented: ECMAScript's Number::toString is shortest-
|
|
169
|
+
* round-trip with its own exponent thresholds, and %g gets 10 wrong (1e+01). */
|
|
170
|
+
std::string NumText(JSContext* ctx, double v) {
|
|
171
|
+
JSValue num = JS_NewFloat64(ctx, v);
|
|
172
|
+
const char* text = JS_ToCString(ctx, num);
|
|
173
|
+
std::string out = text ? text : "";
|
|
174
|
+
if (text) JS_FreeCString(ctx, text);
|
|
175
|
+
JS_FreeValue(ctx, num);
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* `bound` separates "the wrong shape" from "the right shape, out of range",
|
|
180
|
+
* which only the union case needs: it reports a member's bound rather than the
|
|
181
|
+
* generic no-member-matched line, because "above the maximum of 100" is what
|
|
182
|
+
* an operator can act on. */
|
|
183
|
+
struct Failure {
|
|
184
|
+
std::string message;
|
|
185
|
+
std::string path;
|
|
186
|
+
bool bound = false;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
bool FailAs(Failure* out, bool is_bound, const std::string& path, const char* fmt, va_list args) {
|
|
190
|
+
char buf[256];
|
|
191
|
+
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
192
|
+
out->message = buf;
|
|
193
|
+
out->path = path;
|
|
194
|
+
out->bound = is_bound;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
bool Fail(Failure* out, const std::string& path, const char* fmt, ...) {
|
|
199
|
+
va_list args;
|
|
200
|
+
va_start(args, fmt);
|
|
201
|
+
bool r = FailAs(out, false, path, fmt, args);
|
|
202
|
+
va_end(args);
|
|
203
|
+
return r;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
bool FailBound(Failure* out, const std::string& path, const char* fmt, ...) {
|
|
207
|
+
va_list args;
|
|
208
|
+
va_start(args, fmt);
|
|
209
|
+
bool r = FailAs(out, true, path, fmt, args);
|
|
210
|
+
va_end(args);
|
|
211
|
+
return r;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
bool ValidateNode(JSContext* ctx, JSValueConst schema, JSValueConst value, const std::string& path,
|
|
215
|
+
Failure* fail);
|
|
216
|
+
|
|
217
|
+
/* Every field of an object node, in shape order. Optional fields may be
|
|
218
|
+
* absent; a required field that is absent is the error, not a bad value. */
|
|
219
|
+
bool ValidateObjectFields(JSContext* ctx, JSValueConst schema, JSValueConst value,
|
|
220
|
+
const std::string& path, Failure* fail) {
|
|
221
|
+
JSValue shape = JS_GetPropertyStr(ctx, schema, "shape");
|
|
222
|
+
OwnKeys keys(ctx, shape);
|
|
223
|
+
bool ok = true;
|
|
224
|
+
for (uint32_t i = 0; ok && i < keys.len; i++) {
|
|
225
|
+
JSAtom key = keys.tab[i].atom;
|
|
226
|
+
const char* key_text = JS_AtomToCString(ctx, key);
|
|
227
|
+
std::string field_path = path + "." + (key_text ? key_text : "");
|
|
228
|
+
if (key_text) JS_FreeCString(ctx, key_text);
|
|
229
|
+
|
|
230
|
+
JSValue field = JS_GetProperty(ctx, shape, key);
|
|
231
|
+
NodeKind fk = KindOf(ctx, field);
|
|
232
|
+
bool present = HasOwn(ctx, value, key);
|
|
233
|
+
|
|
234
|
+
if (fk.kind == Kind::Optional) {
|
|
235
|
+
if (present) {
|
|
236
|
+
JSValue held = JS_GetProperty(ctx, value, key);
|
|
237
|
+
ok = ValidateNode(ctx, field, held, field_path, fail);
|
|
238
|
+
JS_FreeValue(ctx, held);
|
|
239
|
+
}
|
|
240
|
+
} else if (!present) {
|
|
241
|
+
ok = Fail(fail, field_path, "missing required field");
|
|
242
|
+
} else {
|
|
243
|
+
JSValue held = JS_GetProperty(ctx, value, key);
|
|
244
|
+
ok = ValidateNode(ctx, field, held, field_path, fail);
|
|
245
|
+
JS_FreeValue(ctx, held);
|
|
246
|
+
}
|
|
247
|
+
JS_FreeValue(ctx, field);
|
|
248
|
+
}
|
|
249
|
+
JS_FreeValue(ctx, shape);
|
|
250
|
+
return ok;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
bool ValidateTaggedUnion(JSContext* ctx, JSValueConst schema, JSValueConst value,
|
|
254
|
+
const std::string& path, Failure* fail) {
|
|
255
|
+
if (!JS_IsObject(value) || JS_IsNull(value) || JS_IsArray(value)) {
|
|
256
|
+
return Fail(fail, path, "expected object, got %s", TypeOf(ctx, value));
|
|
257
|
+
}
|
|
258
|
+
JSValue key_val = JS_GetPropertyStr(ctx, schema, "key");
|
|
259
|
+
const char* key_text = JS_ToCString(ctx, key_val);
|
|
260
|
+
std::string key_name = key_text ? key_text : "";
|
|
261
|
+
if (key_text) JS_FreeCString(ctx, key_text);
|
|
262
|
+
JS_FreeValue(ctx, key_val);
|
|
263
|
+
|
|
264
|
+
std::string tag_path = path + "." + key_name;
|
|
265
|
+
/* Plain read, matching core.ts: an inherited member here is not a hole,
|
|
266
|
+
* it fails the primitive check below. */
|
|
267
|
+
JSValue tag = JS_GetPropertyStr(ctx, value, key_name.c_str());
|
|
268
|
+
if (JS_IsUndefined(tag)) {
|
|
269
|
+
JS_FreeValue(ctx, tag);
|
|
270
|
+
return Fail(fail, tag_path, "missing discriminator field");
|
|
271
|
+
}
|
|
272
|
+
if (!JS_IsString(tag) && !JS_IsNumber(tag) && !JS_IsBool(tag)) {
|
|
273
|
+
std::string got = TypeOf(ctx, tag);
|
|
274
|
+
JS_FreeValue(ctx, tag);
|
|
275
|
+
return Fail(fail, tag_path, "expected primitive discriminator, got %s", got.c_str());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
JSValue branches = JS_GetPropertyStr(ctx, schema, "branches");
|
|
279
|
+
JSAtom tag_atom = JS_ValueToAtom(ctx, tag);
|
|
280
|
+
bool known = tag_atom != JS_ATOM_NULL && HasOwn(ctx, branches, tag_atom);
|
|
281
|
+
bool ok;
|
|
282
|
+
if (!known) {
|
|
283
|
+
std::string text = JsonText(ctx, tag);
|
|
284
|
+
ok = Fail(fail, tag_path, "unknown tag %s", text.c_str());
|
|
285
|
+
} else {
|
|
286
|
+
JSValue branch = JS_GetProperty(ctx, branches, tag_atom);
|
|
287
|
+
ok = ValidateNode(ctx, branch, value, path, fail);
|
|
288
|
+
JS_FreeValue(ctx, branch);
|
|
289
|
+
}
|
|
290
|
+
if (tag_atom != JS_ATOM_NULL) JS_FreeAtom(ctx, tag_atom);
|
|
291
|
+
JS_FreeValue(ctx, branches);
|
|
292
|
+
JS_FreeValue(ctx, tag);
|
|
293
|
+
return ok;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
bool ValidateNode(JSContext* ctx, JSValueConst schema, JSValueConst value, const std::string& path,
|
|
297
|
+
Failure* fail) {
|
|
298
|
+
NodeKind nk = KindOf(ctx, schema);
|
|
299
|
+
switch (nk.kind) {
|
|
300
|
+
case Kind::String: {
|
|
301
|
+
if (!JS_IsString(value)) {
|
|
302
|
+
return Fail(fail, path, "expected string, got %s", TypeOf(ctx, value));
|
|
303
|
+
}
|
|
304
|
+
double bound = 0;
|
|
305
|
+
/* .length, not a byte count: JS measures strings in UTF-16 code
|
|
306
|
+
* units and core.ts compares against that. */
|
|
307
|
+
int64_t chars = 0;
|
|
308
|
+
JS_GetLength(ctx, value, &chars);
|
|
309
|
+
if (NumberAnnotation(ctx, schema, "minLength", &bound) && (double)chars < bound) {
|
|
310
|
+
return FailBound(fail, path, "shorter than %s characters", NumText(ctx, bound).c_str());
|
|
311
|
+
}
|
|
312
|
+
if (NumberAnnotation(ctx, schema, "maxLength", &bound) && (double)chars > bound) {
|
|
313
|
+
return FailBound(fail, path, "longer than %s characters", NumText(ctx, bound).c_str());
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case Kind::Number: {
|
|
319
|
+
/* NaN is rejected, and reports as a number: core.ts builds the
|
|
320
|
+
* message from typeOf(), which does not special-case it. */
|
|
321
|
+
double d = 0;
|
|
322
|
+
if (!JS_IsNumber(value) || JS_ToFloat64(ctx, &d, value) != 0 || std::isnan(d)) {
|
|
323
|
+
return Fail(fail, path, "expected number, got %s", TypeOf(ctx, value));
|
|
324
|
+
}
|
|
325
|
+
double bound = 0;
|
|
326
|
+
if (IsTrue(ctx, schema, "integer") && (!std::isfinite(d) || d != std::trunc(d))) {
|
|
327
|
+
return FailBound(fail, path, "expected a whole number, got %s", NumText(ctx, d).c_str());
|
|
328
|
+
}
|
|
329
|
+
if (NumberAnnotation(ctx, schema, "min", &bound) && d < bound) {
|
|
330
|
+
return FailBound(fail, path, "below the minimum of %s", NumText(ctx, bound).c_str());
|
|
331
|
+
}
|
|
332
|
+
if (NumberAnnotation(ctx, schema, "max", &bound) && d > bound) {
|
|
333
|
+
return FailBound(fail, path, "above the maximum of %s", NumText(ctx, bound).c_str());
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case Kind::Boolean:
|
|
339
|
+
if (!JS_IsBool(value)) {
|
|
340
|
+
return Fail(fail, path, "expected boolean, got %s", TypeOf(ctx, value));
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
|
|
344
|
+
case Kind::Unknown:
|
|
345
|
+
return true;
|
|
346
|
+
|
|
347
|
+
case Kind::Literal: {
|
|
348
|
+
JSValue expected = JS_GetPropertyStr(ctx, schema, "value");
|
|
349
|
+
bool same = JS_IsStrictEqual(ctx, value, expected);
|
|
350
|
+
bool ok = true;
|
|
351
|
+
if (!same) {
|
|
352
|
+
std::string want = JsonText(ctx, expected);
|
|
353
|
+
std::string got = JsonText(ctx, value);
|
|
354
|
+
ok = Fail(fail, path, "expected %s, got %s", want.c_str(), got.c_str());
|
|
355
|
+
}
|
|
356
|
+
JS_FreeValue(ctx, expected);
|
|
357
|
+
return ok;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
case Kind::Array: {
|
|
361
|
+
if (!JS_IsArray(value)) {
|
|
362
|
+
return Fail(fail, path, "expected array, got %s", TypeOf(ctx, value));
|
|
363
|
+
}
|
|
364
|
+
int64_t len = 0;
|
|
365
|
+
JS_GetLength(ctx, value, &len);
|
|
366
|
+
double bound = 0;
|
|
367
|
+
if (NumberAnnotation(ctx, schema, "minItems", &bound) && (double)len < bound) {
|
|
368
|
+
return FailBound(fail, path, "fewer than %s items", NumText(ctx, bound).c_str());
|
|
369
|
+
}
|
|
370
|
+
if (NumberAnnotation(ctx, schema, "maxItems", &bound) && (double)len > bound) {
|
|
371
|
+
return FailBound(fail, path, "more than %s items", NumText(ctx, bound).c_str());
|
|
372
|
+
}
|
|
373
|
+
JSValue element = JS_GetPropertyStr(ctx, schema, "element");
|
|
374
|
+
bool ok = true;
|
|
375
|
+
for (int64_t i = 0; ok && i < len; i++) {
|
|
376
|
+
JSValue item = JS_GetPropertyInt64(ctx, value, i);
|
|
377
|
+
ok = ValidateNode(ctx, element, item, path + "[" + std::to_string(i) + "]", fail);
|
|
378
|
+
JS_FreeValue(ctx, item);
|
|
379
|
+
}
|
|
380
|
+
JS_FreeValue(ctx, element);
|
|
381
|
+
return ok;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case Kind::Object:
|
|
385
|
+
if (!JS_IsObject(value) || JS_IsNull(value) || JS_IsArray(value)) {
|
|
386
|
+
return Fail(fail, path, "expected object, got %s", TypeOf(ctx, value));
|
|
387
|
+
}
|
|
388
|
+
return ValidateObjectFields(ctx, schema, value, path, fail);
|
|
389
|
+
|
|
390
|
+
case Kind::Tuple: {
|
|
391
|
+
if (!JS_IsArray(value)) {
|
|
392
|
+
return Fail(fail, path, "expected array, got %s", TypeOf(ctx, value));
|
|
393
|
+
}
|
|
394
|
+
JSValue elements = JS_GetPropertyStr(ctx, schema, "elements");
|
|
395
|
+
int64_t want = 0;
|
|
396
|
+
int64_t got = 0;
|
|
397
|
+
JS_GetLength(ctx, elements, &want);
|
|
398
|
+
JS_GetLength(ctx, value, &got);
|
|
399
|
+
if (want != got) {
|
|
400
|
+
JS_FreeValue(ctx, elements);
|
|
401
|
+
return Fail(fail, path, "expected %lld elements, got %lld", (long long)want,
|
|
402
|
+
(long long)got);
|
|
403
|
+
}
|
|
404
|
+
bool ok = true;
|
|
405
|
+
for (int64_t i = 0; ok && i < want; i++) {
|
|
406
|
+
JSValue node = JS_GetPropertyInt64(ctx, elements, i);
|
|
407
|
+
JSValue item = JS_GetPropertyInt64(ctx, value, i);
|
|
408
|
+
ok = ValidateNode(ctx, node, item, path + "[" + std::to_string(i) + "]", fail);
|
|
409
|
+
JS_FreeValue(ctx, item);
|
|
410
|
+
JS_FreeValue(ctx, node);
|
|
411
|
+
}
|
|
412
|
+
JS_FreeValue(ctx, elements);
|
|
413
|
+
return ok;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case Kind::Optional: {
|
|
417
|
+
if (JS_IsUndefined(value)) return true;
|
|
418
|
+
JSValue inner = JS_GetPropertyStr(ctx, schema, "inner");
|
|
419
|
+
bool ok = ValidateNode(ctx, inner, value, path, fail);
|
|
420
|
+
JS_FreeValue(ctx, inner);
|
|
421
|
+
return ok;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
case Kind::Union: {
|
|
425
|
+
JSValue members = JS_GetPropertyStr(ctx, schema, "members");
|
|
426
|
+
int64_t len = 0;
|
|
427
|
+
JS_GetLength(ctx, members, &len);
|
|
428
|
+
/* A union accepts what ANY member accepts, bounds included: in
|
|
429
|
+
* union([number({max: 10}), number({min: 100})]), 150 matches the
|
|
430
|
+
* first member's shape, breaks its bound, and the second member
|
|
431
|
+
* exists for exactly that value. When nothing passes, a member
|
|
432
|
+
* that only broke a bound gives the better message; a member of
|
|
433
|
+
* the wrong shape says nothing useful about a mixed union. */
|
|
434
|
+
bool matched = false;
|
|
435
|
+
bool have_range = false;
|
|
436
|
+
Failure out_of_range;
|
|
437
|
+
for (int64_t i = 0; !matched && i < len; i++) {
|
|
438
|
+
JSValue member = JS_GetPropertyInt64(ctx, members, i);
|
|
439
|
+
Failure member_fail;
|
|
440
|
+
matched = ValidateNode(ctx, member, value, path, &member_fail);
|
|
441
|
+
if (!matched && member_fail.bound && !have_range) {
|
|
442
|
+
out_of_range = member_fail;
|
|
443
|
+
have_range = true;
|
|
444
|
+
}
|
|
445
|
+
JS_FreeValue(ctx, member);
|
|
446
|
+
}
|
|
447
|
+
JS_FreeValue(ctx, members);
|
|
448
|
+
if (matched) return true;
|
|
449
|
+
if (have_range) {
|
|
450
|
+
*fail = out_of_range;
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
return Fail(fail, path, "value did not match any union member");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case Kind::TaggedUnion:
|
|
457
|
+
return ValidateTaggedUnion(ctx, schema, value, path, fail);
|
|
458
|
+
|
|
459
|
+
case Kind::Invalid:
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
return Fail(fail, path, "unknown schema kind: %s", nk.raw.c_str());
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
JSValue ValidationError(JSContext* ctx, const Failure& fail) {
|
|
466
|
+
JSValue error = JS_NewObject(ctx);
|
|
467
|
+
JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, "ValidationFailed"),
|
|
468
|
+
JS_PROP_C_W_E);
|
|
469
|
+
JS_DefinePropertyValueStr(ctx, error, "message", JS_NewString(ctx, fail.message.c_str()),
|
|
470
|
+
JS_PROP_C_W_E);
|
|
471
|
+
JS_DefinePropertyValueStr(ctx, error, "path", JS_NewString(ctx, fail.path.c_str()),
|
|
472
|
+
JS_PROP_C_W_E);
|
|
473
|
+
/* The plain {ok, error} shape core.ts's local err() builds, not a
|
|
474
|
+
* mikro/result Result: parse() in the JS facade wraps this. */
|
|
475
|
+
JSValue out = JS_NewObject(ctx);
|
|
476
|
+
JS_DefinePropertyValueStr(ctx, out, "ok", JS_FALSE, JS_PROP_C_W_E);
|
|
477
|
+
JS_DefinePropertyValueStr(ctx, out, "error", error, JS_PROP_C_W_E);
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
JSValue js_validate(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
482
|
+
JSValueConst schema = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
483
|
+
JSValueConst value = argc > 1 ? argv[1] : JS_UNDEFINED;
|
|
484
|
+
std::string path;
|
|
485
|
+
if (argc > 2 && JS_IsString(argv[2])) {
|
|
486
|
+
const char* text = JS_ToCString(ctx, argv[2]);
|
|
487
|
+
if (text) {
|
|
488
|
+
path = text;
|
|
489
|
+
JS_FreeCString(ctx, text);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
Failure fail;
|
|
493
|
+
bool ok = ValidateNode(ctx, schema, value, path, &fail);
|
|
494
|
+
/* A malformed schema (a Symbol kind, a missing shape) can leave an
|
|
495
|
+
* exception pending mid-walk; surface it here rather than at whatever
|
|
496
|
+
* unrelated call runs next. */
|
|
497
|
+
if (JS_HasException(ctx)) return JS_EXCEPTION;
|
|
498
|
+
if (ok) return JS_NULL;
|
|
499
|
+
return ValidationError(ctx, fail);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* ── applyDefaults ──────────────────────────────────────────────────── */
|
|
503
|
+
|
|
504
|
+
JSValue ApplyDefaults(JSContext* ctx, JSValueConst schema, JSValueConst value) {
|
|
505
|
+
NodeKind nk = KindOf(ctx, schema);
|
|
506
|
+
switch (nk.kind) {
|
|
507
|
+
case Kind::Object: {
|
|
508
|
+
/* A present non-object stays as-is so validation rejects it. */
|
|
509
|
+
if (!JS_IsUndefined(value) &&
|
|
510
|
+
(!JS_IsObject(value) || JS_IsNull(value) || JS_IsArray(value))) {
|
|
511
|
+
return JS_DupValue(ctx, value);
|
|
512
|
+
}
|
|
513
|
+
JSValue shape = JS_GetPropertyStr(ctx, schema, "shape");
|
|
514
|
+
JSValue out = JS_NewObject(ctx);
|
|
515
|
+
OwnKeys keys(ctx, shape);
|
|
516
|
+
for (uint32_t i = 0; i < keys.len; i++) {
|
|
517
|
+
JSAtom key = keys.tab[i].atom;
|
|
518
|
+
JSValue field = JS_GetProperty(ctx, shape, key);
|
|
519
|
+
bool present = !JS_IsUndefined(value) && HasOwn(ctx, value, key);
|
|
520
|
+
JSValue raw = present ? JS_GetProperty(ctx, value, key) : JS_UNDEFINED;
|
|
521
|
+
|
|
522
|
+
NodeKind fk = KindOf(ctx, field);
|
|
523
|
+
if (fk.kind == Kind::Optional) {
|
|
524
|
+
if (!JS_IsUndefined(raw)) {
|
|
525
|
+
JS_DefinePropertyValue(ctx, out, key, JS_DupValue(ctx, raw),
|
|
526
|
+
JS_PROP_C_W_E);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
JSValue child = ApplyDefaults(ctx, field, raw);
|
|
530
|
+
if (!JS_IsUndefined(child)) {
|
|
531
|
+
JS_DefinePropertyValue(ctx, out, key, child, JS_PROP_C_W_E);
|
|
532
|
+
} else {
|
|
533
|
+
JS_FreeValue(ctx, child);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
JS_FreeValue(ctx, raw);
|
|
537
|
+
JS_FreeValue(ctx, field);
|
|
538
|
+
}
|
|
539
|
+
JS_FreeValue(ctx, shape);
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case Kind::Array: {
|
|
544
|
+
if (!JS_IsUndefined(value)) return JS_DupValue(ctx, value);
|
|
545
|
+
JSValue fallback = JS_GetPropertyStr(ctx, schema, "default");
|
|
546
|
+
if (!JS_IsUndefined(fallback)) return fallback;
|
|
547
|
+
JS_FreeValue(ctx, fallback);
|
|
548
|
+
return JS_NewArray(ctx);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
case Kind::Unknown:
|
|
552
|
+
case Kind::Optional:
|
|
553
|
+
return JS_DupValue(ctx, value);
|
|
554
|
+
|
|
555
|
+
default: {
|
|
556
|
+
if (!JS_IsUndefined(value)) return JS_DupValue(ctx, value);
|
|
557
|
+
return JS_GetPropertyStr(ctx, schema, "default");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
JSValue js_apply_defaults(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
563
|
+
JSValueConst schema = argc > 0 ? argv[0] : JS_UNDEFINED;
|
|
564
|
+
JSValueConst value = argc > 1 ? argv[1] : JS_UNDEFINED;
|
|
565
|
+
JSValue out = ApplyDefaults(ctx, schema, value);
|
|
566
|
+
if (JS_HasException(ctx)) {
|
|
567
|
+
JS_FreeValue(ctx, out);
|
|
568
|
+
return JS_EXCEPTION;
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* ── Constructors ───────────────────────────────────────────────────── */
|
|
574
|
+
|
|
575
|
+
/* Every annotation any constructor accepts, in the order core.ts copies them,
|
|
576
|
+
* so a schema serializes to the same JSON from either implementation. */
|
|
577
|
+
const char* const kAnnotationKeys[] = {"title", "description", "mask", "minLength",
|
|
578
|
+
"maxLength", "min", "max", "integer",
|
|
579
|
+
"minItems", "maxItems", "format", "unit"};
|
|
580
|
+
|
|
581
|
+
bool HasDefault(JSContext* ctx, JSValueConst node) {
|
|
582
|
+
JSValue d = JS_GetPropertyStr(ctx, node, "default");
|
|
583
|
+
bool present = !JS_IsUndefined(d);
|
|
584
|
+
JS_FreeValue(ctx, d);
|
|
585
|
+
return present;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* Defaults below a wholesale unit never fill, so they are rejected where they
|
|
589
|
+
* are written rather than at the validation that later misses the field. */
|
|
590
|
+
bool RejectInnerDefaults(JSContext* ctx, JSValueConst node, const std::string& path,
|
|
591
|
+
const char* unit, const char* self) {
|
|
592
|
+
if (HasDefault(ctx, node)) {
|
|
593
|
+
JS_ThrowTypeError(ctx,
|
|
594
|
+
"a default under %s never applies; give %s itself a whole-value "
|
|
595
|
+
"default instead (found at %s)",
|
|
596
|
+
unit, self, path.c_str());
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
NodeKind nk = KindOf(ctx, node);
|
|
600
|
+
if (nk.kind == Kind::Object) {
|
|
601
|
+
JSValue shape = JS_GetPropertyStr(ctx, node, "shape");
|
|
602
|
+
OwnKeys keys(ctx, shape);
|
|
603
|
+
bool ok = true;
|
|
604
|
+
for (uint32_t i = 0; ok && i < keys.len; i++) {
|
|
605
|
+
const char* key_text = JS_AtomToCString(ctx, keys.tab[i].atom);
|
|
606
|
+
std::string child_path = path + "." + (key_text ? key_text : "");
|
|
607
|
+
if (key_text) JS_FreeCString(ctx, key_text);
|
|
608
|
+
JSValue field = JS_GetProperty(ctx, shape, keys.tab[i].atom);
|
|
609
|
+
ok = RejectInnerDefaults(ctx, field, child_path, unit, self);
|
|
610
|
+
JS_FreeValue(ctx, field);
|
|
611
|
+
}
|
|
612
|
+
JS_FreeValue(ctx, shape);
|
|
613
|
+
return ok;
|
|
614
|
+
}
|
|
615
|
+
if (nk.kind == Kind::Optional) {
|
|
616
|
+
/* optional() rejects an inner default itself, so this only reaches
|
|
617
|
+
* what it wraps without reporting the same node twice. */
|
|
618
|
+
JSValue inner = JS_GetPropertyStr(ctx, node, "inner");
|
|
619
|
+
bool ok = RejectInnerDefaults(ctx, inner, path, unit, self);
|
|
620
|
+
JS_FreeValue(ctx, inner);
|
|
621
|
+
return ok;
|
|
622
|
+
}
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Copies the annotations onto the node and rejects a default whose shape the
|
|
627
|
+
* node would not accept. Consumes `node`; returns it, or an exception. */
|
|
628
|
+
JSValue Annotate(JSContext* ctx, JSValue node, JSValueConst options) {
|
|
629
|
+
if (JS_IsUndefined(options) || JS_IsNull(options)) return node;
|
|
630
|
+
|
|
631
|
+
for (const char* key : kAnnotationKeys) {
|
|
632
|
+
JSValue v = JS_GetPropertyStr(ctx, options, key);
|
|
633
|
+
if (JS_IsUndefined(v)) {
|
|
634
|
+
JS_FreeValue(ctx, v);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
JS_DefinePropertyValueStr(ctx, node, key, v, JS_PROP_C_W_E);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
JSValue fallback = JS_GetPropertyStr(ctx, options, "default");
|
|
641
|
+
if (JS_IsUndefined(fallback)) {
|
|
642
|
+
JS_FreeValue(ctx, fallback);
|
|
643
|
+
return node;
|
|
644
|
+
}
|
|
645
|
+
Failure fail;
|
|
646
|
+
bool ok = ValidateNode(ctx, node, fallback, "", &fail);
|
|
647
|
+
JS_DefinePropertyValueStr(ctx, node, "default", fallback, JS_PROP_C_W_E);
|
|
648
|
+
if (!ok) {
|
|
649
|
+
JS_FreeValue(ctx, node);
|
|
650
|
+
JS_ThrowTypeError(ctx, "schema default does not match the schema: %s",
|
|
651
|
+
fail.message.c_str());
|
|
652
|
+
return JS_EXCEPTION;
|
|
653
|
+
}
|
|
654
|
+
return node;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
JSValue NewNode(JSContext* ctx, const char* kind) {
|
|
658
|
+
JSValue node = JS_NewObject(ctx);
|
|
659
|
+
JS_DefinePropertyValueStr(ctx, node, "kind", JS_NewString(ctx, kind), JS_PROP_C_W_E);
|
|
660
|
+
return node;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
JSValueConst ArgAt(int argc, JSValueConst* argv, int i) {
|
|
664
|
+
return i < argc ? argv[i] : JS_UNDEFINED;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
JSValue js_string(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
668
|
+
return Annotate(ctx, NewNode(ctx, "string"), ArgAt(argc, argv, 0));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
JSValue js_number(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
672
|
+
return Annotate(ctx, NewNode(ctx, "number"), ArgAt(argc, argv, 0));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
JSValue js_boolean(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
676
|
+
return Annotate(ctx, NewNode(ctx, "boolean"), ArgAt(argc, argv, 0));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
JSValue js_unknown(JSContext* ctx, JSValueConst, int, JSValueConst*) {
|
|
680
|
+
return NewNode(ctx, "unknown");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
JSValue js_literal(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
684
|
+
JSValue node = NewNode(ctx, "literal");
|
|
685
|
+
JS_DefinePropertyValueStr(ctx, node, "value", JS_DupValue(ctx, ArgAt(argc, argv, 0)),
|
|
686
|
+
JS_PROP_C_W_E);
|
|
687
|
+
return Annotate(ctx, node, ArgAt(argc, argv, 1));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
JSValue js_array(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
691
|
+
JSValueConst element = ArgAt(argc, argv, 0);
|
|
692
|
+
if (!RejectInnerDefaults(ctx, element, "[]", "an array", "the array")) return JS_EXCEPTION;
|
|
693
|
+
JSValue node = NewNode(ctx, "array");
|
|
694
|
+
JS_DefinePropertyValueStr(ctx, node, "element", JS_DupValue(ctx, element), JS_PROP_C_W_E);
|
|
695
|
+
return Annotate(ctx, node, ArgAt(argc, argv, 1));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
JSValue js_object(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
699
|
+
JSValueConst options = ArgAt(argc, argv, 1);
|
|
700
|
+
if (JS_IsObject(options)) {
|
|
701
|
+
JSValue fallback = JS_GetPropertyStr(ctx, options, "default");
|
|
702
|
+
bool has = !JS_IsUndefined(fallback);
|
|
703
|
+
JS_FreeValue(ctx, fallback);
|
|
704
|
+
if (has) {
|
|
705
|
+
return JS_ThrowTypeError(
|
|
706
|
+
ctx, "an object's defaults compose from its fields; declare defaults on the "
|
|
707
|
+
"fields");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
JSValue node = NewNode(ctx, "object");
|
|
711
|
+
JS_DefinePropertyValueStr(ctx, node, "shape", JS_DupValue(ctx, ArgAt(argc, argv, 0)),
|
|
712
|
+
JS_PROP_C_W_E);
|
|
713
|
+
return Annotate(ctx, node, options);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
JSValue js_tuple(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
717
|
+
JSValueConst elements = ArgAt(argc, argv, 0);
|
|
718
|
+
int64_t len = 0;
|
|
719
|
+
JS_GetLength(ctx, elements, &len);
|
|
720
|
+
for (int64_t i = 0; i < len; i++) {
|
|
721
|
+
JSValue item = JS_GetPropertyInt64(ctx, elements, i);
|
|
722
|
+
bool ok = RejectInnerDefaults(ctx, item, "[" + std::to_string(i) + "]", "a tuple",
|
|
723
|
+
"the tuple");
|
|
724
|
+
JS_FreeValue(ctx, item);
|
|
725
|
+
if (!ok) return JS_EXCEPTION;
|
|
726
|
+
}
|
|
727
|
+
JSValue node = NewNode(ctx, "tuple");
|
|
728
|
+
JS_DefinePropertyValueStr(ctx, node, "elements", JS_DupValue(ctx, elements), JS_PROP_C_W_E);
|
|
729
|
+
return Annotate(ctx, node, ArgAt(argc, argv, 1));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
JSValue js_optional(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
733
|
+
JSValueConst inner = ArgAt(argc, argv, 0);
|
|
734
|
+
if (HasDefault(ctx, inner)) {
|
|
735
|
+
return JS_ThrowTypeError(ctx, "optional() cannot wrap a schema with a default");
|
|
736
|
+
}
|
|
737
|
+
JSValue node = NewNode(ctx, "optional");
|
|
738
|
+
JS_DefinePropertyValueStr(ctx, node, "inner", JS_DupValue(ctx, inner), JS_PROP_C_W_E);
|
|
739
|
+
return node;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
JSValue js_union(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
743
|
+
JSValueConst members = ArgAt(argc, argv, 0);
|
|
744
|
+
int64_t len = 0;
|
|
745
|
+
JS_GetLength(ctx, members, &len);
|
|
746
|
+
for (int64_t i = 0; i < len; i++) {
|
|
747
|
+
JSValue item = JS_GetPropertyInt64(ctx, members, i);
|
|
748
|
+
bool ok =
|
|
749
|
+
RejectInnerDefaults(ctx, item, "[" + std::to_string(i) + "]", "a union", "the union");
|
|
750
|
+
JS_FreeValue(ctx, item);
|
|
751
|
+
if (!ok) return JS_EXCEPTION;
|
|
752
|
+
}
|
|
753
|
+
JSValue node = NewNode(ctx, "union");
|
|
754
|
+
JS_DefinePropertyValueStr(ctx, node, "members", JS_DupValue(ctx, members), JS_PROP_C_W_E);
|
|
755
|
+
return Annotate(ctx, node, ArgAt(argc, argv, 1));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* Sugar, not a node kind: a union of annotated literals, so nothing
|
|
759
|
+
* downstream has to learn about it. */
|
|
760
|
+
JSValue js_enum_of(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
761
|
+
JSValueConst entries = ArgAt(argc, argv, 0);
|
|
762
|
+
int64_t len = 0;
|
|
763
|
+
JS_GetLength(ctx, entries, &len);
|
|
764
|
+
JSValue members = JS_NewArray(ctx);
|
|
765
|
+
for (int64_t i = 0; i < len; i++) {
|
|
766
|
+
JSValue entry = JS_GetPropertyInt64(ctx, entries, i);
|
|
767
|
+
JSValue node = NewNode(ctx, "literal");
|
|
768
|
+
JSValue value = JS_GetPropertyStr(ctx, entry, "value");
|
|
769
|
+
JS_DefinePropertyValueStr(ctx, node, "value", value, JS_PROP_C_W_E);
|
|
770
|
+
for (const char* key : {"title", "description"}) {
|
|
771
|
+
JSValue v = JS_GetPropertyStr(ctx, entry, key);
|
|
772
|
+
if (JS_IsUndefined(v)) {
|
|
773
|
+
JS_FreeValue(ctx, v);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
JS_DefinePropertyValueStr(ctx, node, key, v, JS_PROP_C_W_E);
|
|
777
|
+
}
|
|
778
|
+
JS_FreeValue(ctx, entry);
|
|
779
|
+
JS_SetPropertyInt64(ctx, members, i, node);
|
|
780
|
+
}
|
|
781
|
+
JSValue out = NewNode(ctx, "union");
|
|
782
|
+
JS_DefinePropertyValueStr(ctx, out, "members", members, JS_PROP_C_W_E);
|
|
783
|
+
return Annotate(ctx, out, ArgAt(argc, argv, 1));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
JSValue js_tagged_union(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
787
|
+
JSValueConst key = ArgAt(argc, argv, 0);
|
|
788
|
+
JSValueConst branches = ArgAt(argc, argv, 1);
|
|
789
|
+
{
|
|
790
|
+
OwnKeys tags(ctx, branches);
|
|
791
|
+
for (uint32_t i = 0; i < tags.len; i++) {
|
|
792
|
+
const char* tag_text = JS_AtomToCString(ctx, tags.tab[i].atom);
|
|
793
|
+
std::string tag_path = std::string(".") + (tag_text ? tag_text : "");
|
|
794
|
+
if (tag_text) JS_FreeCString(ctx, tag_text);
|
|
795
|
+
JSValue branch = JS_GetProperty(ctx, branches, tags.tab[i].atom);
|
|
796
|
+
bool ok = RejectInnerDefaults(ctx, branch, tag_path, "a taggedUnion", "the union");
|
|
797
|
+
JS_FreeValue(ctx, branch);
|
|
798
|
+
if (!ok) return JS_EXCEPTION;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
JSValue node = NewNode(ctx, "taggedUnion");
|
|
802
|
+
JS_DefinePropertyValueStr(ctx, node, "key", JS_DupValue(ctx, key), JS_PROP_C_W_E);
|
|
803
|
+
JS_DefinePropertyValueStr(ctx, node, "branches", JS_DupValue(ctx, branches), JS_PROP_C_W_E);
|
|
804
|
+
return Annotate(ctx, node, ArgAt(argc, argv, 2));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* SchemaError.ValidationFailed(message, path) — the error factory the public
|
|
808
|
+
* surface exposes, distinct from the {ok, error} envelope validate() returns. */
|
|
809
|
+
JSValue js_validation_failed(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
|
|
810
|
+
JSValue error = JS_NewObject(ctx);
|
|
811
|
+
JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, "ValidationFailed"),
|
|
812
|
+
JS_PROP_C_W_E);
|
|
813
|
+
JS_DefinePropertyValueStr(ctx, error, "message", JS_DupValue(ctx, ArgAt(argc, argv, 0)),
|
|
814
|
+
JS_PROP_C_W_E);
|
|
815
|
+
JS_DefinePropertyValueStr(ctx, error, "path", JS_DupValue(ctx, ArgAt(argc, argv, 1)),
|
|
816
|
+
JS_PROP_C_W_E);
|
|
817
|
+
return error;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* ── Module registration ────────────────────────────────────────────── */
|
|
821
|
+
|
|
822
|
+
constexpr const char* kModuleName = "native:mikro/schema";
|
|
823
|
+
MIK__REQUIRE_NATIVE_NS("native:mikro/schema");
|
|
824
|
+
|
|
825
|
+
/* One table drives both JS_AddModuleExport and JS_SetModuleExport. Declaring a
|
|
826
|
+
* name in only one of the two resolves to undefined at the import site, which
|
|
827
|
+
* is why they are not written out separately here. */
|
|
828
|
+
struct SchemaExport {
|
|
829
|
+
const char* name;
|
|
830
|
+
JSCFunction* func; /* null for SchemaError, which is a namespace object */
|
|
831
|
+
int arity;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const SchemaExport kSchemaExports[] = {
|
|
835
|
+
{"string", js_string, 1},
|
|
836
|
+
{"number", js_number, 1},
|
|
837
|
+
{"boolean", js_boolean, 1},
|
|
838
|
+
{"unknown", js_unknown, 0},
|
|
839
|
+
{"literal", js_literal, 2},
|
|
840
|
+
{"array", js_array, 2},
|
|
841
|
+
{"object", js_object, 2},
|
|
842
|
+
{"tuple", js_tuple, 2},
|
|
843
|
+
{"optional", js_optional, 1},
|
|
844
|
+
{"union", js_union, 2},
|
|
845
|
+
{"enumOf", js_enum_of, 2},
|
|
846
|
+
{"taggedUnion", js_tagged_union, 3},
|
|
847
|
+
{"applyDefaults", js_apply_defaults, 2},
|
|
848
|
+
{"validate", js_validate, 3},
|
|
849
|
+
{"SchemaError", nullptr, 0},
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
JSValue NewSchemaErrorNamespace(JSContext* ctx) {
|
|
853
|
+
JSValue ns = JS_NewObject(ctx);
|
|
854
|
+
JS_DefinePropertyValueStr(ctx, ns, "ValidationFailed",
|
|
855
|
+
JS_NewCFunction(ctx, js_validation_failed, "ValidationFailed", 2),
|
|
856
|
+
JS_PROP_C_W_E);
|
|
857
|
+
return ns;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
int mik__schema_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
861
|
+
for (const SchemaExport& e : kSchemaExports) {
|
|
862
|
+
JSValue value = e.func ? JS_NewCFunction(ctx, e.func, e.name, e.arity)
|
|
863
|
+
: NewSchemaErrorNamespace(ctx);
|
|
864
|
+
JS_SetModuleExport(ctx, m, e.name, value);
|
|
865
|
+
}
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
JSModuleDef* mik__schema_init(JSContext* ctx) {
|
|
870
|
+
JSModuleDef* m = JS_NewCModule(ctx, kModuleName, mik__schema_module_init);
|
|
871
|
+
if (!m) return nullptr;
|
|
872
|
+
for (const SchemaExport& e : kSchemaExports) JS_AddModuleExport(ctx, m, e.name);
|
|
873
|
+
return m;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
} // namespace
|
|
877
|
+
|
|
878
|
+
/* Lazy: the descriptor goes on the global registry and mik__schema_init runs
|
|
879
|
+
* on first import of native:mikro/schema, so an app that never imports
|
|
880
|
+
* mikro/schema pays nothing. Declaring the module eagerly instead cost every
|
|
881
|
+
* runtime about 4 KB, most of it an atom-table resize triggered by the export
|
|
882
|
+
* names. MIK_REGISTER_MODULE would give the same laziness, but its constructor
|
|
883
|
+
* attribute does not survive static-library linking into the Node addon —
|
|
884
|
+
* mik_cbor.cpp registers explicitly for the same reason. */
|
|
885
|
+
void mik__schema_register(void) {
|
|
886
|
+
static mik_module_desc_t desc = {kModuleName, mik__schema_init, nullptr, nullptr, nullptr};
|
|
887
|
+
static bool registered = false;
|
|
888
|
+
if (registered) return;
|
|
889
|
+
registered = true;
|
|
890
|
+
desc.next = mik__module_registry_head;
|
|
891
|
+
mik__module_registry_head = &desc;
|
|
892
|
+
}
|