@oh-my-pi/omptype 17.2.6

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/src/infer.ts ADDED
@@ -0,0 +1,160 @@
1
+ /** Type-level output inference for the definition forms accepted by omptype. */
2
+
3
+ type Whitespace = " " | "\n" | "\r" | "\t";
4
+
5
+ type TrimLeft<s extends string> = s extends `${Whitespace}${infer rest}` ? TrimLeft<rest> : s;
6
+ type TrimRight<s extends string> = s extends `${infer rest}${Whitespace}` ? TrimRight<rest> : s;
7
+ type Trim<s extends string> = TrimLeft<TrimRight<s>>;
8
+
9
+ type InferPrimitive<s extends string> = s extends "string" | "string.url"
10
+ ? string
11
+ : s extends "number" | "number.integer"
12
+ ? number
13
+ : s extends "boolean"
14
+ ? boolean
15
+ : s extends "null"
16
+ ? null
17
+ : s extends "undefined"
18
+ ? undefined
19
+ : s extends "unknown"
20
+ ? unknown
21
+ : s extends "object"
22
+ ? object
23
+ : s extends "bigint"
24
+ ? bigint
25
+ : s extends "true"
26
+ ? true
27
+ : s extends "false"
28
+ ? false
29
+ : never;
30
+
31
+ type InferMember<member extends string> =
32
+ Trim<member> extends infer s extends string
33
+ ? s extends `${infer element}[]`
34
+ ? InferMember<element>[]
35
+ : s extends `'${infer literal}'` | `"${infer literal}"`
36
+ ? literal
37
+ : s extends `${infer literal extends number}`
38
+ ? literal
39
+ : InferPrimitive<s> extends infer primitive
40
+ ? [primitive] extends [never]
41
+ ? s extends `${string}string${string}`
42
+ ? string
43
+ : s extends `${string}number${string}`
44
+ ? number
45
+ : unknown
46
+ : primitive
47
+ : unknown
48
+ : unknown;
49
+
50
+ /** Split top-level unions without distributing over the accumulated members. */
51
+ type InferUnion<s extends string, result = never> = s extends `${infer head}|${infer tail}`
52
+ ? InferUnion<tail, result | InferMember<head>>
53
+ : result | InferMember<s>;
54
+
55
+ type HasInlineDefault<s extends string> = s extends `${string}=${string}`
56
+ ? s extends `${string}<${string}` | `${string}>${string}`
57
+ ? false
58
+ : true
59
+ : false;
60
+
61
+ type WithoutInlineDefault<s extends string> =
62
+ HasInlineDefault<s> extends true ? (s extends `${infer base}=${string}` ? Trim<base> : s) : s;
63
+
64
+ /** String-DSL-only inference; safe inside recursive interfaces (never probes `Type`). */
65
+ export type InferString<s extends string> =
66
+ WithoutInlineDefault<Trim<s>> extends infer trimmed extends string
67
+ ? trimmed extends `${infer base}?`
68
+ ? InferString<base>
69
+ : trimmed extends `(${infer inner})[]`
70
+ ? InferUnion<inner>[]
71
+ : InferUnion<trimmed>
72
+ : unknown;
73
+
74
+ type HasDefault<def> = def extends string
75
+ ? HasInlineDefault<def>
76
+ : def extends { readonly hasDefault: true }
77
+ ? true
78
+ : false;
79
+
80
+ type DefinitionKeys<def extends object> = Exclude<keyof def, "+" | "[string]">;
81
+
82
+ /** Optional when the key carries a `?` suffix or the string value does (`limit: "number?"`). */
83
+ type IsOptionalProp<key, def> = key extends `${string}?`
84
+ ? true
85
+ : def extends string
86
+ ? Trim<def> extends `${string}?`
87
+ ? true
88
+ : false
89
+ : false;
90
+
91
+ type PropName<key extends PropertyKey> = key extends `${infer name}?` ? name : key;
92
+
93
+ type RequiredProperties<def extends object> = {
94
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
95
+ ? HasDefault<def[key]> extends true
96
+ ? PropName<key>
97
+ : never
98
+ : PropName<key>]-?: InferDef<def[key]>;
99
+ };
100
+
101
+ type OptionalProperties<def extends object> = {
102
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
103
+ ? HasDefault<def[key]> extends true
104
+ ? never
105
+ : PropName<key>
106
+ : never]?: InferDef<def[key]>;
107
+ };
108
+
109
+ type Simplify<t> = { [key in keyof t]: t[key] };
110
+ type PropertiesOf<def extends object> = Simplify<RequiredProperties<def> & OptionalProperties<def>>;
111
+
112
+ type InferObject<def extends object> = "[string]" extends keyof def
113
+ ? [DefinitionKeys<def>] extends [never]
114
+ ? Record<string, InferDef<def["[string]"]>>
115
+ : PropertiesOf<def> & Record<string, InferDef<def["[string]"]>>
116
+ : PropertiesOf<def>;
117
+
118
+ /** Object-literal inference for fluent composition without re-inspecting the containing `Type`. */
119
+ export type InferObjectDef<def extends object> = InferObject<def>;
120
+
121
+ type InferLiteralDef<def> = def extends string
122
+ ? InferString<def>
123
+ : def extends object
124
+ ? InferObjectLiteral<def>
125
+ : unknown;
126
+
127
+ type LiteralRequiredProperties<def extends object> = {
128
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
129
+ ? HasDefault<def[key]> extends true
130
+ ? PropName<key>
131
+ : never
132
+ : PropName<key>]-?: InferLiteralDef<def[key]>;
133
+ };
134
+
135
+ type LiteralOptionalProperties<def extends object> = {
136
+ -readonly [key in DefinitionKeys<def> as IsOptionalProp<key, def[key]> extends true
137
+ ? HasDefault<def[key]> extends true
138
+ ? never
139
+ : PropName<key>
140
+ : never]?: InferLiteralDef<def[key]>;
141
+ };
142
+
143
+ /** Object-literal-only inference for fluent branches; never inspects embedded schema types. */
144
+ export type InferObjectLiteral<def extends object> = Simplify<
145
+ LiteralRequiredProperties<def> & LiteralOptionalProperties<def>
146
+ >;
147
+
148
+ /** Infer the validated output type produced by a definition. */
149
+ export type InferDef<def = unknown> = def extends { infer: infer output }
150
+ ? output
151
+ : def extends string
152
+ ? InferString<def>
153
+ : def extends readonly [infer element, "[]"]
154
+ ? InferDef<element>[]
155
+ : def extends object
156
+ ? InferObject<def>
157
+ : unknown;
158
+
159
+ /** Input-side defaults and morphs are not distinguished yet. */
160
+ export type InferDefIn<def = unknown> = InferDef<def>;
package/src/interp.ts ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Tree-walking validator used for a schema's first few calls (before the JIT
3
+ * compiler kicks in) and as the fallback for IR shapes the compiler declines.
4
+ *
5
+ * Semantics must stay in lockstep with `compile.ts`:
6
+ * - success returns the output value; the input is returned as-is unless the
7
+ * schema morphs (defaults, `"+": "delete"`, embedded stepped schemas), in
8
+ * which case a fresh object/array is produced and the input is untouched
9
+ * - failure returns an `OmpErrors` with a single fast-fail entry
10
+ */
11
+ import { MISSING, OmpErrors } from "./errors";
12
+ import { expectedOf, hasMorph, type IR } from "./ir";
13
+
14
+ const own = Object.prototype.hasOwnProperty;
15
+
16
+ /** Validate `value` against `ir`; returns output value or `OmpErrors`. */
17
+ export function walk(ir: IR, value: unknown): unknown {
18
+ const path: PropertyKey[] = [];
19
+ const out = visit(ir, value, path);
20
+ return out;
21
+ }
22
+
23
+ function fail(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
24
+ return OmpErrors.single([...path], expected, data);
25
+ }
26
+
27
+ /** Pure predicate used for union-member scanning (no morphs, no errors). */
28
+ function checks(ir: IR, v: unknown): boolean {
29
+ switch (ir.k) {
30
+ case "unknown":
31
+ return true;
32
+ case "null":
33
+ return v === null;
34
+ case "undefined":
35
+ return v === undefined;
36
+ case "boolean":
37
+ return typeof v === "boolean";
38
+ case "bigint":
39
+ return typeof v === "bigint";
40
+ case "anyobject":
41
+ return typeof v === "object" && v !== null;
42
+ case "string":
43
+ return (
44
+ typeof v === "string" &&
45
+ (ir.min === undefined || v.length >= ir.min) &&
46
+ (ir.max === undefined || v.length <= ir.max) &&
47
+ (!ir.url || URL.canParse(v))
48
+ );
49
+ case "number":
50
+ return (
51
+ typeof v === "number" &&
52
+ (!ir.int || Number.isInteger(v)) &&
53
+ (ir.min === undefined || (ir.xmin ? v > ir.min : v >= ir.min)) &&
54
+ (ir.max === undefined || (ir.xmax ? v < ir.max : v <= ir.max))
55
+ );
56
+ case "lit":
57
+ return v === ir.v;
58
+ case "union":
59
+ return ir.members.some(m => checks(m, v));
60
+ case "array": {
61
+ if (!Array.isArray(v)) return false;
62
+ if (ir.min !== undefined && v.length < ir.min) return false;
63
+ if (ir.max !== undefined && v.length > ir.max) return false;
64
+ for (const el of v) if (!checks(ir.el, el)) return false;
65
+ return true;
66
+ }
67
+ case "object": {
68
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
69
+ const rec = v as Record<string, unknown>;
70
+ for (const p of ir.props) {
71
+ const present = p.key in rec;
72
+ if (!present) {
73
+ if (!p.opt && !p.hasDefault) return false;
74
+ continue;
75
+ }
76
+ if (!checks(p.val, rec[p.key])) return false;
77
+ }
78
+ if (ir.index) {
79
+ for (const key in rec) {
80
+ if (own.call(rec, key) && !checks(ir.index, rec[key])) return false;
81
+ }
82
+ } else if (ir.extras === "reject") {
83
+ for (const key in rec) {
84
+ if (!own.call(rec, key)) continue;
85
+ let declared = false;
86
+ for (const p of ir.props) {
87
+ if (p.key === key) {
88
+ declared = true;
89
+ break;
90
+ }
91
+ }
92
+ if (!declared) return false;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ case "sub":
98
+ return !(ir.schema.run(v) instanceof OmpErrors);
99
+ }
100
+ }
101
+
102
+ function visit(ir: IR, v: unknown, path: PropertyKey[]): unknown {
103
+ switch (ir.k) {
104
+ case "sub": {
105
+ const out = ir.schema.run(v);
106
+ if (out instanceof OmpErrors) {
107
+ return path.length === 0 ? out : prefixAll(out, path);
108
+ }
109
+ return out;
110
+ }
111
+ case "union": {
112
+ // fast path: any pure member matching returns the input unchanged
113
+ for (const m of ir.members) {
114
+ if (m.k !== "sub" && checks(m, v)) {
115
+ if (hasMorph(m)) break;
116
+ return v;
117
+ }
118
+ }
119
+ for (const m of ir.members) {
120
+ if (m.k === "sub" || hasMorph(m)) {
121
+ const out = visit(m, v, path);
122
+ if (!(out instanceof OmpErrors)) return out;
123
+ }
124
+ }
125
+ return unionFail(ir, v, path);
126
+ }
127
+ case "array": {
128
+ if (!Array.isArray(v)) return fail(path, "an array", v);
129
+ if (ir.min !== undefined && v.length < ir.min) return fail(path, `at least length ${ir.min}`, v);
130
+ if (ir.max !== undefined && v.length > ir.max) return fail(path, `at most length ${ir.max}`, v);
131
+ if (!hasMorph(ir.el)) {
132
+ for (let i = 0; i < v.length; i++) {
133
+ if (!checks(ir.el, v[i])) {
134
+ path.push(i);
135
+ const err = visit(ir.el, v[i], path);
136
+ path.pop();
137
+ return err instanceof OmpErrors ? err : fail([...path, i], expectedOf(ir.el), v[i]);
138
+ }
139
+ }
140
+ return v;
141
+ }
142
+ const out = new Array<unknown>(v.length);
143
+ for (let i = 0; i < v.length; i++) {
144
+ path.push(i);
145
+ const el = visit(ir.el, v[i], path);
146
+ path.pop();
147
+ if (el instanceof OmpErrors) return el;
148
+ out[i] = el;
149
+ }
150
+ return out;
151
+ }
152
+ case "object": {
153
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return fail(path, "an object", v);
154
+ const rec = v as Record<string, unknown>;
155
+ const morph = hasMorph(ir);
156
+ let out: Record<string, unknown> | undefined;
157
+ if (morph) {
158
+ if (ir.extras === "delete" && !ir.index) {
159
+ out = {};
160
+ } else {
161
+ out = { ...rec };
162
+ }
163
+ }
164
+ for (const p of ir.props) {
165
+ if (!(p.key in rec)) {
166
+ if (p.hasDefault && out) {
167
+ // defFactory guarantees a callable default payload
168
+ const payload = p.def;
169
+ out[p.key] = p.defFactory && typeof payload === "function" ? payload() : payload;
170
+ continue;
171
+ }
172
+ if (p.opt || p.hasDefault) continue;
173
+ path.push(p.key);
174
+ const err = fail(path, expectedOf(p.val), MISSING);
175
+ path.pop();
176
+ return err;
177
+ }
178
+ path.push(p.key);
179
+ const res = visit(p.val, rec[p.key], path);
180
+ path.pop();
181
+ if (res instanceof OmpErrors) return res;
182
+ if (out) out[p.key] = res;
183
+ }
184
+ if (ir.index) {
185
+ for (const key in rec) {
186
+ if (!own.call(rec, key)) continue;
187
+ path.push(key);
188
+ const res = visit(ir.index, rec[key], path);
189
+ path.pop();
190
+ if (res instanceof OmpErrors) return res;
191
+ if (out) out[key] = res;
192
+ }
193
+ } else if (ir.extras === "reject") {
194
+ for (const key in rec) {
195
+ if (!own.call(rec, key)) continue;
196
+ let declared = false;
197
+ for (const p of ir.props) {
198
+ if (p.key === key) {
199
+ declared = true;
200
+ break;
201
+ }
202
+ }
203
+ if (!declared) {
204
+ path.push(key);
205
+ const err = fail(path, "removed (undeclared key)", rec[key]);
206
+ path.pop();
207
+ return err;
208
+ }
209
+ }
210
+ }
211
+ return out ?? v;
212
+ }
213
+ default:
214
+ return checks(ir, v) ? v : fail(path, expectedOf(ir), v);
215
+ }
216
+ }
217
+
218
+ function prefixAll(errs: OmpErrors, path: PropertyKey[]): OmpErrors {
219
+ for (let i = path.length - 1; i >= 0; i--) errs.prefix(path[i]);
220
+ return errs;
221
+ }
222
+
223
+ /**
224
+ * Detailed failure for a union: descend into the member the value was clearly
225
+ * aimed at — unique runtime-kind match, else an object member whose literal
226
+ * discriminant property (e.g. `type: "'computer_call'"`) equals the value's —
227
+ * for a precise nested error (paths, narrow messages) instead of the coarse
228
+ * "A or B" expectation.
229
+ */
230
+ export function unionFail(ir: IR & { k: "union" }, v: unknown, path: PropertyKey[]): OmpErrors {
231
+ let best: IR | undefined;
232
+ for (const m of ir.members) {
233
+ const base = m.k === "sub" ? m.schema.ir : m;
234
+ if (!kindMatches(base, v)) continue;
235
+ if (best !== undefined) {
236
+ best = undefined;
237
+ break;
238
+ }
239
+ best = m;
240
+ }
241
+ if (best === undefined) best = discriminate(ir.members, v);
242
+ if (best) {
243
+ const out = visit(best, v, path);
244
+ if (out instanceof OmpErrors) return out;
245
+ }
246
+ return fail(path, expectedOf(ir), v);
247
+ }
248
+
249
+ /** Pick the sole object member whose literal-typed property matches the value's. */
250
+ function discriminate(members: IR[], v: unknown): IR | undefined {
251
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return undefined;
252
+ const rec = v as Record<string, unknown>;
253
+ let match: IR | undefined;
254
+ for (const m of members) {
255
+ const base = m.k === "sub" ? m.schema.ir : m;
256
+ if (base.k !== "object") continue;
257
+ for (const p of base.props) {
258
+ if (p.val.k !== "lit" || rec[p.key] !== p.val.v) continue;
259
+ if (match !== undefined) return undefined; // ambiguous
260
+ match = m;
261
+ break;
262
+ }
263
+ }
264
+ return match;
265
+ }
266
+
267
+ /** True when a value's runtime shape could only be aimed at this member. */
268
+ function kindMatches(base: IR, v: unknown): boolean {
269
+ switch (base.k) {
270
+ case "array":
271
+ return Array.isArray(v);
272
+ case "object":
273
+ case "anyobject":
274
+ return typeof v === "object" && v !== null && !Array.isArray(v);
275
+ case "string":
276
+ return typeof v === "string";
277
+ case "number":
278
+ return typeof v === "number";
279
+ default:
280
+ return false;
281
+ }
282
+ }