@ultimat3/schema 31.6.0 → 31.7.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/CLAUDE.md +5 -0
- package/package.json +1 -1
- package/src/array-schema.ts +2 -1
- package/src/builder.ts +43 -6
- package/src/record-schema.ts +4 -4
- package/src/validators.ts +2 -1
package/CLAUDE.md
CHANGED
|
@@ -23,6 +23,11 @@ loop: **a loop that collects issues asks `stopCollecting(count, hasMore)` after
|
|
|
23
23
|
union that discards member issues restores `issuesStopped()`. `standard.large.test.ts` pins 10k bad
|
|
24
24
|
items → 100 issues, `issuesTruncated`, ≤ 100 item checks (a work count, never wall time). The cap is ambient module state because builtin
|
|
25
25
|
validation is synchronous; `withIssueCap` is re-entrant and only the outermost run reports.
|
|
26
|
+
**A composite reads a builtin member's `Check` directly** (`checkOf` answers it from a `WeakMap`
|
|
27
|
+
keyed by the member's `'~standard'` object, filled in `makeSchema`; only a foreign Standard Schema
|
|
28
|
+
member goes through `validate`), and calls it through `checkAt(check, value, path, key)`: the
|
|
29
|
+
member runs at a shared empty path and its issues are re-rooted only on failure.
|
|
30
|
+
`standard.perf.test.ts` pins both as work counts (0 public validates, 1 path for 10k valid rows).
|
|
26
31
|
`schema-rules.ts` is a leaf (`SCHEMA_RULES`, `issueRuleFacts`), imported by `standard.ts`,
|
|
27
32
|
`errors.ts` (type only) and `builder.ts`. `sensitive.ts` reads `provider.ts` and is not in `t`'s graph.
|
|
28
33
|
|
package/package.json
CHANGED
package/src/array-schema.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
type AnySchema,
|
|
7
|
+
checkAt,
|
|
7
8
|
checkOf,
|
|
8
9
|
failWith,
|
|
9
10
|
makeSchema,
|
|
@@ -83,7 +84,7 @@ export function arraySchema<S extends AnySchema>(
|
|
|
83
84
|
const issues: StandardIssue[] = [];
|
|
84
85
|
const out: unknown[] = [];
|
|
85
86
|
for (const [index, member] of value.entries()) {
|
|
86
|
-
const result = itemCheck
|
|
87
|
+
const result = checkAt(itemCheck, member, path, index);
|
|
87
88
|
if (result.ok) out.push(result.value);
|
|
88
89
|
else {
|
|
89
90
|
issues.push(...result.issues);
|
package/src/builder.ts
CHANGED
|
@@ -253,13 +253,23 @@ function capped<Out>(check: Check<Out>, value: unknown): StandardResult<Out> {
|
|
|
253
253
|
: cappedFailure(run.value.issues, run.cap, run.stopped);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* A builtin schema's check, keyed by its `'~standard'` object — the one part every spread
|
|
258
|
+
* (`{ ...base, min }`) keeps by reference. A composite reads a builtin member through this, never
|
|
259
|
+
* through the public `validate`: that door costs a cap closure, a run record, a result and a
|
|
260
|
+
* re-wrap per member even when the member passes (`standard.perf.test.ts`).
|
|
261
|
+
*/
|
|
262
|
+
const BUILTIN_CHECKS = new WeakMap<object, Check<unknown>>();
|
|
263
|
+
|
|
256
264
|
export function makeSchema<In, Out>(node: SchemaNode, check: Check<Out>): Schema<In, Out> {
|
|
265
|
+
const props: Schema<In, Out>['~standard'] = {
|
|
266
|
+
version: 1,
|
|
267
|
+
vendor: VENDOR,
|
|
268
|
+
validate: (value: unknown): StandardResult<Out> => capped(check, value),
|
|
269
|
+
};
|
|
270
|
+
BUILTIN_CHECKS.set(props, check as Check<unknown>);
|
|
257
271
|
const schema: Schema<In, Out> = {
|
|
258
|
-
'~standard':
|
|
259
|
-
version: 1,
|
|
260
|
-
vendor: VENDOR,
|
|
261
|
-
validate: (value: unknown): StandardResult<Out> => capped(check, value),
|
|
262
|
-
},
|
|
272
|
+
'~standard': props,
|
|
263
273
|
node,
|
|
264
274
|
parse(value: unknown, root = 'value'): Out {
|
|
265
275
|
const result = capped(check, value);
|
|
@@ -329,8 +339,14 @@ export function makeSchema<In, Out>(node: SchemaNode, check: Check<Out>): Schema
|
|
|
329
339
|
return schema;
|
|
330
340
|
}
|
|
331
341
|
|
|
332
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* Read the check back off a schema so composites can reuse it. A builtin schema answers its own
|
|
344
|
+
* check, joining the caller's issue cap directly; a foreign Standard Schema member keeps the
|
|
345
|
+
* public route, its issues re-rooted under the caller's path.
|
|
346
|
+
*/
|
|
333
347
|
export function checkOf<In, Out>(schema: Schema<In, Out>): Check<Out> {
|
|
348
|
+
const builtin = BUILTIN_CHECKS.get(schema['~standard']);
|
|
349
|
+
if (builtin !== undefined) return builtin as Check<Out>;
|
|
334
350
|
return (value, path) => {
|
|
335
351
|
const result = schema['~standard'].validate(value);
|
|
336
352
|
if (isThenable(result)) {
|
|
@@ -348,3 +364,24 @@ export function checkOf<In, Out>(schema: Schema<In, Out>): Check<Out> {
|
|
|
348
364
|
);
|
|
349
365
|
};
|
|
350
366
|
}
|
|
367
|
+
|
|
368
|
+
/** The path a composite hands a member: shared, never written to, so a pass allocates none. */
|
|
369
|
+
const MEMBER_ROOT: Path = Object.freeze([]);
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A composite's member at `key` under `path`. The member runs at a shared empty path and its
|
|
373
|
+
* issues are re-rooted only when it fails, so a passing member costs no path array; order and
|
|
374
|
+
* count are the member's own, so the issue cap and issue order are untouched.
|
|
375
|
+
*/
|
|
376
|
+
export function checkAt<Out>(
|
|
377
|
+
check: Check<Out>,
|
|
378
|
+
value: unknown,
|
|
379
|
+
path: Path,
|
|
380
|
+
key: PropertyKey,
|
|
381
|
+
): CheckResult<Out> {
|
|
382
|
+
const result = check(value, MEMBER_ROOT);
|
|
383
|
+
if (result.ok) return result;
|
|
384
|
+
return failWith(
|
|
385
|
+
result.issues.map((issue) => ({ ...issue, path: [...path, key, ...(issue.path ?? [])] })),
|
|
386
|
+
);
|
|
387
|
+
}
|
package/src/record-schema.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
type AnySchema,
|
|
7
|
+
checkAt,
|
|
7
8
|
checkOf,
|
|
8
9
|
failWith,
|
|
9
10
|
isPlainObject,
|
|
@@ -49,14 +50,13 @@ export function recordSchema<S extends AnySchema>(
|
|
|
49
50
|
// the same segment `arraySchema` already uses, so `formatPath` renders `meta[3]` with no
|
|
50
51
|
// new spelling to learn. It is `Object.entries` order — integer-like keys sort ahead of
|
|
51
52
|
// string ones — so it names an entry that exists rather than a byte offset in the body.
|
|
52
|
-
const at = [...path, index];
|
|
53
53
|
// A record is stored as `jsonb` far more often than not, and jsonb refuses an unstorable
|
|
54
54
|
// KEY exactly as it does a value — so the key is refused here, by position, not quoted.
|
|
55
55
|
const unfitKey = unstorable(key);
|
|
56
56
|
if (unfitKey !== undefined) {
|
|
57
57
|
issues.push({
|
|
58
58
|
message: `expected a record key, received one ${unfitKey}`,
|
|
59
|
-
path:
|
|
59
|
+
path: [...path, index],
|
|
60
60
|
rule: 'record.key',
|
|
61
61
|
});
|
|
62
62
|
continue;
|
|
@@ -66,12 +66,12 @@ export function recordSchema<S extends AnySchema>(
|
|
|
66
66
|
// The refused NAMES are a closed set declared right here, so the message may state
|
|
67
67
|
// them: that is the one part of this line the caller did not choose.
|
|
68
68
|
message: expected(`a record key that is not ${PROTOTYPE_KEYS.join(' | ')}`, key),
|
|
69
|
-
path:
|
|
69
|
+
path: [...path, index],
|
|
70
70
|
rule: 'record.key',
|
|
71
71
|
});
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
|
-
const result = valueCheck
|
|
74
|
+
const result = checkAt(valueCheck, entry, path, index);
|
|
75
75
|
if (result.ok) out[key] = result.value;
|
|
76
76
|
else issues.push(...result.issues);
|
|
77
77
|
}
|
package/src/validators.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type ArrayBounds, arraySchema } from './array-schema';
|
|
|
7
7
|
import {
|
|
8
8
|
type AnySchema,
|
|
9
9
|
type Check,
|
|
10
|
+
checkAt,
|
|
10
11
|
checkOf,
|
|
11
12
|
failWith,
|
|
12
13
|
isPlainObject,
|
|
@@ -224,7 +225,7 @@ export function objectSchema<S extends Shape>(shape: S): ObjectSchema<S> {
|
|
|
224
225
|
// its `.default()` could never fire, and the HTTP path and this one disagreed about what
|
|
225
226
|
// the caller sent.
|
|
226
227
|
const raw = Object.hasOwn(value, key) ? value[key] : undefined;
|
|
227
|
-
const result = memberCheck
|
|
228
|
+
const result = checkAt(memberCheck, raw, path, key);
|
|
228
229
|
if (result.ok) {
|
|
229
230
|
// Unknown keys are dropped, never forwarded — no mass assignment through an action.
|
|
230
231
|
if (result.value !== undefined) out[key] = result.value;
|