@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/CHANGELOG.md +8 -0
- package/README.md +60 -0
- package/dist/types/ark.d.ts +26 -0
- package/dist/types/compile.d.ts +5 -0
- package/dist/types/errors.d.ts +63 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/infer.d.ts +49 -0
- package/dist/types/interp.d.ts +24 -0
- package/dist/types/ir.d.ts +115 -0
- package/dist/types/json-schema.d.ts +8 -0
- package/dist/types/type.d.ts +100 -0
- package/dist/types/typebox.d.ts +184 -0
- package/dist/types/zod.d.ts +100 -0
- package/package.json +56 -0
- package/src/ark.ts +30 -0
- package/src/compile.ts +415 -0
- package/src/errors.ts +136 -0
- package/src/index.ts +13 -0
- package/src/infer.ts +160 -0
- package/src/interp.ts +282 -0
- package/src/ir.ts +480 -0
- package/src/json-schema.ts +172 -0
- package/src/type.ts +329 -0
- package/src/typebox.ts +487 -0
- package/src/zod.ts +339 -0
package/src/zod.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { OmpTypeError } from "./errors";
|
|
2
|
+
import { type EmbeddableSchema, embed, type IR, IR_BRAND, type PropIR } from "./ir";
|
|
3
|
+
import { type FluentType, type NarrowContext, type Type, type } from "./type";
|
|
4
|
+
|
|
5
|
+
interface OptionalSchemaMarker {
|
|
6
|
+
readonly _optional: true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RefineOptions {
|
|
10
|
+
message?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ZodLikeIssue {
|
|
15
|
+
path: PropertyKey[];
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ZodLikeSafeParseResult<Out> =
|
|
20
|
+
| { success: true; data: Out }
|
|
21
|
+
| { success: false; error: { message: string; issues: ZodLikeIssue[] } };
|
|
22
|
+
|
|
23
|
+
/** A callable omptype schema carrying the Zod-v4-style fluent surface. */
|
|
24
|
+
export interface ZodLikeSchema<out Out> extends Type<Out> {
|
|
25
|
+
readonly _output: Out;
|
|
26
|
+
/** @internal Used while composing object property IR. */
|
|
27
|
+
readonly isOptional: boolean;
|
|
28
|
+
parse(value: unknown): Out;
|
|
29
|
+
safeParse(value: unknown): ZodLikeSafeParseResult<Out>;
|
|
30
|
+
min(bound: number): ZodLikeSchema<Out>;
|
|
31
|
+
max(bound: number): ZodLikeSchema<Out>;
|
|
32
|
+
int(): ZodLikeSchema<Out>;
|
|
33
|
+
positive(): ZodLikeSchema<Out>;
|
|
34
|
+
nonnegative(): ZodLikeSchema<Out>;
|
|
35
|
+
regex(expression: RegExp, message?: string): ZodLikeSchema<Out>;
|
|
36
|
+
url(): ZodLikeSchema<Out>;
|
|
37
|
+
optional(): ZodLikeSchema<Out | undefined> & OptionalSchemaMarker;
|
|
38
|
+
nullable(): ZodLikeSchema<Out | null>;
|
|
39
|
+
default(value: Exclude<Out, undefined> | (() => Exclude<Out, undefined>)): ZodLikeSchema<Exclude<Out, undefined>>;
|
|
40
|
+
describe(description: string): ZodLikeSchema<Out>;
|
|
41
|
+
refine(predicate: (value: Out) => unknown, messageOrOptions?: string | RefineOptions): ZodLikeSchema<Out>;
|
|
42
|
+
transform<Next>(transformer: (value: Out) => Next): ZodLikeSchema<Next>;
|
|
43
|
+
catch(fallback: Out | (() => Out)): ZodLikeSchema<Out>;
|
|
44
|
+
strict(): ZodLikeSchema<Out>;
|
|
45
|
+
passthrough(): ZodLikeSchema<Out & Record<string, unknown>>;
|
|
46
|
+
strip(): ZodLikeSchema<Out>;
|
|
47
|
+
partial(): Out extends object ? ZodLikeSchema<Partial<Out>> : ZodLikeSchema<Out>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function schemaFromIR<Out>(ir: IR): FluentType<Out> {
|
|
51
|
+
const embedded: EmbeddableSchema = {
|
|
52
|
+
[IR_BRAND]: true,
|
|
53
|
+
ir,
|
|
54
|
+
hasSteps: false,
|
|
55
|
+
hasDefault: false,
|
|
56
|
+
run: value => value,
|
|
57
|
+
};
|
|
58
|
+
return type.raw(embedded) as FluentType<Out>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withMeta<Out>(source: FluentType<Out>, target: FluentType<Out>): FluentType<Out> {
|
|
62
|
+
let next = target;
|
|
63
|
+
if (source.description !== undefined) next = next.describe(source.description);
|
|
64
|
+
if (source.hasDefault) next = next.default(source.defaultValue as Out | (() => Out));
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function restrictBase<Out>(source: FluentType<Out>, ir: IR): FluentType<Out> {
|
|
69
|
+
let next = schemaFromIR<Out>(ir);
|
|
70
|
+
if (source.hasSteps) next = next.pipe(value => source(value)) as FluentType<Out>;
|
|
71
|
+
return withMeta(source, next);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function lengthBound(kind: "min" | "max", schema: FluentType<unknown>, bound: number): void {
|
|
75
|
+
if (schema.ir.k !== "string" && schema.ir.k !== "array") return;
|
|
76
|
+
if (!Number.isSafeInteger(bound) || bound < 0) {
|
|
77
|
+
throw new OmpTypeError(`${kind} length must be a nonnegative safe integer`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function refinementMessage(messageOrOptions: string | RefineOptions | undefined): string {
|
|
82
|
+
if (typeof messageOrOptions === "string") return messageOrOptions;
|
|
83
|
+
return messageOrOptions?.message ?? messageOrOptions?.error ?? "valid (refinement failed)";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isStringKeyIR(ir: IR): boolean {
|
|
87
|
+
switch (ir.k) {
|
|
88
|
+
case "string":
|
|
89
|
+
return true;
|
|
90
|
+
case "lit":
|
|
91
|
+
return typeof ir.v === "string";
|
|
92
|
+
case "union":
|
|
93
|
+
return ir.members.length > 0 && ir.members.every(isStringKeyIR);
|
|
94
|
+
case "sub":
|
|
95
|
+
return isStringKeyIR(ir.schema.ir);
|
|
96
|
+
default:
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function decorate<Out>(schema: FluentType<Out>, optional = false): ZodLikeSchema<Out> {
|
|
102
|
+
const next = (inner: FluentType<Out>, nextOptional = optional): ZodLikeSchema<Out> => decorate(inner, nextOptional);
|
|
103
|
+
const withObjectExtras = (extras: "keep" | "reject" | "delete"): ZodLikeSchema<Out> => {
|
|
104
|
+
if (schema.ir.k !== "object") throw new OmpTypeError("object mode requires an object schema");
|
|
105
|
+
return next(restrictBase(schema, { ...schema.ir, extras }));
|
|
106
|
+
};
|
|
107
|
+
Object.defineProperty(schema, "isOptional", { value: optional, enumerable: false });
|
|
108
|
+
|
|
109
|
+
return Object.assign(schema, {
|
|
110
|
+
parse(value: unknown): Out {
|
|
111
|
+
const result = schema(value);
|
|
112
|
+
if (result instanceof type.errors) throw new Error(result.summary);
|
|
113
|
+
return result;
|
|
114
|
+
},
|
|
115
|
+
safeParse(value: unknown): ZodLikeSafeParseResult<Out> {
|
|
116
|
+
const result = schema(value);
|
|
117
|
+
if (!(result instanceof type.errors)) return { success: true, data: result };
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: {
|
|
121
|
+
message: result.summary,
|
|
122
|
+
issues: result.map(issue => ({ path: [...issue.path], message: issue.problem })),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
min(bound: number): ZodLikeSchema<Out> {
|
|
127
|
+
const ir = schema.ir;
|
|
128
|
+
if (ir.k === "string" || ir.k === "array") {
|
|
129
|
+
lengthBound("min", schema, bound);
|
|
130
|
+
const min = ir.min === undefined ? bound : Math.max(ir.min, bound);
|
|
131
|
+
return next(restrictBase(schema, { ...ir, min }));
|
|
132
|
+
}
|
|
133
|
+
if (ir.k === "number") {
|
|
134
|
+
if (Number.isNaN(bound)) throw new OmpTypeError("number min must not be NaN");
|
|
135
|
+
if (ir.min !== undefined && ir.min >= bound) return next(restrictBase(schema, ir));
|
|
136
|
+
return next(restrictBase(schema, { ...ir, min: bound, xmin: false }));
|
|
137
|
+
}
|
|
138
|
+
throw new OmpTypeError(`cannot apply min to ${ir.k}`);
|
|
139
|
+
},
|
|
140
|
+
max(bound: number): ZodLikeSchema<Out> {
|
|
141
|
+
const ir = schema.ir;
|
|
142
|
+
if (ir.k === "string" || ir.k === "array") {
|
|
143
|
+
lengthBound("max", schema, bound);
|
|
144
|
+
const max = ir.max === undefined ? bound : Math.min(ir.max, bound);
|
|
145
|
+
return next(restrictBase(schema, { ...ir, max }));
|
|
146
|
+
}
|
|
147
|
+
if (ir.k === "number") {
|
|
148
|
+
if (Number.isNaN(bound)) throw new OmpTypeError("number max must not be NaN");
|
|
149
|
+
if (ir.max !== undefined && ir.max <= bound) return next(restrictBase(schema, ir));
|
|
150
|
+
return next(restrictBase(schema, { ...ir, max: bound, xmax: false }));
|
|
151
|
+
}
|
|
152
|
+
throw new OmpTypeError(`cannot apply max to ${ir.k}`);
|
|
153
|
+
},
|
|
154
|
+
int(): ZodLikeSchema<Out> {
|
|
155
|
+
if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply int to ${schema.ir.k}`);
|
|
156
|
+
return next(restrictBase(schema, { ...schema.ir, int: true }));
|
|
157
|
+
},
|
|
158
|
+
positive(): ZodLikeSchema<Out> {
|
|
159
|
+
if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply positive to ${schema.ir.k}`);
|
|
160
|
+
const ir = schema.ir;
|
|
161
|
+
if (ir.min !== undefined && ir.min > 0) return next(restrictBase(schema, ir));
|
|
162
|
+
return next(restrictBase(schema, { ...ir, min: 0, xmin: true }));
|
|
163
|
+
},
|
|
164
|
+
nonnegative(): ZodLikeSchema<Out> {
|
|
165
|
+
if (schema.ir.k !== "number") throw new OmpTypeError(`cannot apply nonnegative to ${schema.ir.k}`);
|
|
166
|
+
return this.min(0);
|
|
167
|
+
},
|
|
168
|
+
regex(expression: RegExp, message?: string): ZodLikeSchema<Out> {
|
|
169
|
+
if (schema.ir.k !== "string") throw new OmpTypeError(`cannot apply regex to ${schema.ir.k}`);
|
|
170
|
+
const expectation = message ?? `matching ${expression}`;
|
|
171
|
+
const narrowed = schema.narrow((value, ctx) => {
|
|
172
|
+
expression.lastIndex = 0;
|
|
173
|
+
const matches = expression.test(value as string);
|
|
174
|
+
expression.lastIndex = 0;
|
|
175
|
+
return matches || ctx.mustBe(expectation);
|
|
176
|
+
});
|
|
177
|
+
return next(narrowed);
|
|
178
|
+
},
|
|
179
|
+
url(): ZodLikeSchema<Out> {
|
|
180
|
+
if (schema.ir.k !== "string") throw new OmpTypeError(`cannot apply url to ${schema.ir.k}`);
|
|
181
|
+
return next(restrictBase(schema, { ...schema.ir, url: true }));
|
|
182
|
+
},
|
|
183
|
+
optional(): ZodLikeSchema<Out | undefined> & OptionalSchemaMarker {
|
|
184
|
+
const widened = schema.or(type.raw("undefined")) as FluentType<Out | undefined>;
|
|
185
|
+
return decorate(widened, true) as ZodLikeSchema<Out | undefined> & OptionalSchemaMarker;
|
|
186
|
+
},
|
|
187
|
+
nullable(): ZodLikeSchema<Out | null> {
|
|
188
|
+
return decorate(schema.or(type.raw("null")) as FluentType<Out | null>, optional);
|
|
189
|
+
},
|
|
190
|
+
default(
|
|
191
|
+
value: Exclude<Out, undefined> | (() => Exclude<Out, undefined>),
|
|
192
|
+
): ZodLikeSchema<Exclude<Out, undefined>> {
|
|
193
|
+
type DefaultOut = Exclude<Out, undefined>;
|
|
194
|
+
const widened = schema.or(type.raw("undefined")) as FluentType<Out | undefined>;
|
|
195
|
+
const piped = widened.pipe(output => {
|
|
196
|
+
if (output !== undefined) return output as DefaultOut;
|
|
197
|
+
return typeof value === "function" ? (value as () => DefaultOut)() : value;
|
|
198
|
+
}) as FluentType<DefaultOut>;
|
|
199
|
+
return decorate(piped.default(value as DefaultOut | (() => DefaultOut)));
|
|
200
|
+
},
|
|
201
|
+
describe(description: string): ZodLikeSchema<Out> {
|
|
202
|
+
return next(restrictBase(schema, { ...schema.ir, desc: description }).describe(description));
|
|
203
|
+
},
|
|
204
|
+
refine(predicate: (value: Out) => unknown, messageOrOptions?: string | RefineOptions): ZodLikeSchema<Out> {
|
|
205
|
+
const expectation = refinementMessage(messageOrOptions);
|
|
206
|
+
return next(schema.narrow((value, ctx) => Boolean(predicate(value)) || ctx.mustBe(expectation)));
|
|
207
|
+
},
|
|
208
|
+
transform<Next>(transformer: (value: Out) => Next): ZodLikeSchema<Next> {
|
|
209
|
+
return decorate(
|
|
210
|
+
schema.pipe(value => transformer(value)),
|
|
211
|
+
optional,
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
catch(fallback: Out | (() => Out)): ZodLikeSchema<Out> {
|
|
215
|
+
const caught = type.unknown.pipe(input => {
|
|
216
|
+
try {
|
|
217
|
+
const result = schema(input);
|
|
218
|
+
if (!(result instanceof type.errors)) return result;
|
|
219
|
+
} catch {
|
|
220
|
+
// A caught schema is deliberately total, including user refinement/transform exceptions.
|
|
221
|
+
}
|
|
222
|
+
return typeof fallback === "function" ? (fallback as () => Out)() : fallback;
|
|
223
|
+
});
|
|
224
|
+
return decorate(caught as FluentType<Out>, optional);
|
|
225
|
+
},
|
|
226
|
+
strict(): ZodLikeSchema<Out> {
|
|
227
|
+
return withObjectExtras("reject");
|
|
228
|
+
},
|
|
229
|
+
passthrough(): ZodLikeSchema<Out & Record<string, unknown>> {
|
|
230
|
+
return withObjectExtras("keep") as ZodLikeSchema<Out & Record<string, unknown>>;
|
|
231
|
+
},
|
|
232
|
+
strip(): ZodLikeSchema<Out> {
|
|
233
|
+
return withObjectExtras("delete");
|
|
234
|
+
},
|
|
235
|
+
partial(): Out extends object ? ZodLikeSchema<Partial<Out>> : ZodLikeSchema<Out> {
|
|
236
|
+
if (schema.ir.k !== "object") throw new OmpTypeError(`cannot apply partial to ${schema.ir.k}`);
|
|
237
|
+
const props = schema.ir.props.map(prop => ({ ...prop, opt: true }));
|
|
238
|
+
return next(restrictBase(schema, { ...schema.ir, props })) as Out extends object
|
|
239
|
+
? ZodLikeSchema<Partial<Out>>
|
|
240
|
+
: ZodLikeSchema<Out>;
|
|
241
|
+
},
|
|
242
|
+
}) as unknown as ZodLikeSchema<Out>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type infer<T> = T extends { readonly _output: infer Out } ? Out : never;
|
|
246
|
+
|
|
247
|
+
type SchemaOutput<Schema> = Schema extends { readonly _output: infer Out } ? Out : never;
|
|
248
|
+
type Shape = Readonly<Record<string, ZodLikeSchema<unknown>>>;
|
|
249
|
+
type ObjectOutput<S extends Shape> = {
|
|
250
|
+
-readonly [K in keyof S as S[K] extends OptionalSchemaMarker ? never : K]: SchemaOutput<S[K]>;
|
|
251
|
+
} & {
|
|
252
|
+
-readonly [K in keyof S as S[K] extends OptionalSchemaMarker ? K : never]?: SchemaOutput<S[K]>;
|
|
253
|
+
};
|
|
254
|
+
type Simplify<T> = { [K in keyof T]: T[K] };
|
|
255
|
+
type UnionOutput<Schemas extends readonly ZodLikeSchema<unknown>[]> = SchemaOutput<Schemas[number]>;
|
|
256
|
+
|
|
257
|
+
function objectSchema<const S extends Shape>(shape: S): ZodLikeSchema<Simplify<ObjectOutput<S>>> {
|
|
258
|
+
const props: PropIR[] = [];
|
|
259
|
+
for (const key in shape) {
|
|
260
|
+
const member = shape[key];
|
|
261
|
+
const prop: PropIR = { key, opt: member.isOptional, val: embed(member) };
|
|
262
|
+
if (member.hasDefault) {
|
|
263
|
+
prop.hasDefault = true;
|
|
264
|
+
prop.def = member.defaultValue;
|
|
265
|
+
prop.defFactory = typeof member.defaultValue === "function";
|
|
266
|
+
}
|
|
267
|
+
props.push(prop);
|
|
268
|
+
}
|
|
269
|
+
return decorate(schemaFromIR({ k: "object", props, extras: "delete" }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const string = (): ZodLikeSchema<string> => decorate(schemaFromIR(type.string.ir));
|
|
273
|
+
export const number = (): ZodLikeSchema<number> => decorate(schemaFromIR(type.number.ir));
|
|
274
|
+
export const boolean = (): ZodLikeSchema<boolean> => decorate(schemaFromIR(type.boolean.ir));
|
|
275
|
+
export const literal = <const Value>(value: Value): ZodLikeSchema<Value> => decorate(type.enumerated(value));
|
|
276
|
+
const enumSchema = <const Values extends readonly [string, ...string[]]>(
|
|
277
|
+
values: Values,
|
|
278
|
+
): ZodLikeSchema<Values[number]> => {
|
|
279
|
+
if (values.length === 0) throw new OmpTypeError("enum requires at least one value");
|
|
280
|
+
return decorate(type.enumerated(...values));
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export { enumSchema as enum };
|
|
284
|
+
export const union = <
|
|
285
|
+
const Schemas extends readonly [ZodLikeSchema<unknown>, ZodLikeSchema<unknown>, ...ZodLikeSchema<unknown>[]],
|
|
286
|
+
>(
|
|
287
|
+
schemas: Schemas,
|
|
288
|
+
): ZodLikeSchema<UnionOutput<Schemas>> =>
|
|
289
|
+
decorate(schemaFromIR({ k: "union", members: schemas.map(schema => embed(schema)) }));
|
|
290
|
+
export const array = <Element>(element: ZodLikeSchema<Element>): ZodLikeSchema<Element[]> =>
|
|
291
|
+
decorate(schemaFromIR({ k: "array", el: embed(element) }));
|
|
292
|
+
export const object = <const S extends Shape>(shape: S): ZodLikeSchema<Simplify<ObjectOutput<S>>> =>
|
|
293
|
+
objectSchema(shape);
|
|
294
|
+
export const record = <Key extends string, Value>(
|
|
295
|
+
keySchema: ZodLikeSchema<Key>,
|
|
296
|
+
valueSchema: ZodLikeSchema<Value>,
|
|
297
|
+
): ZodLikeSchema<Record<string, Value>> => {
|
|
298
|
+
if (!isStringKeyIR(keySchema.ir)) throw new OmpTypeError("record keys must use a string schema");
|
|
299
|
+
const base = schemaFromIR<Record<string, Value>>({
|
|
300
|
+
k: "object",
|
|
301
|
+
props: [],
|
|
302
|
+
index: embed(valueSchema),
|
|
303
|
+
extras: "keep",
|
|
304
|
+
});
|
|
305
|
+
const checked = base.narrow((value, ctx: NarrowContext) => {
|
|
306
|
+
for (const key in value) {
|
|
307
|
+
if (keySchema(key) instanceof type.errors) return ctx.mustBe("a record with valid string keys");
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
return decorate(checked);
|
|
312
|
+
};
|
|
313
|
+
export const unknown = (): ZodLikeSchema<unknown> => decorate(schemaFromIR(type.unknown.ir));
|
|
314
|
+
export const any = (): ZodLikeSchema<unknown> => decorate(schemaFromIR(type.unknown.ir));
|
|
315
|
+
const nullSchema = (): ZodLikeSchema<null> => decorate(type.raw("null") as FluentType<null>);
|
|
316
|
+
const undefinedSchema = (): ZodLikeSchema<undefined> => decorate(type.raw("undefined") as FluentType<undefined>);
|
|
317
|
+
|
|
318
|
+
export { nullSchema as null, undefinedSchema as undefined };
|
|
319
|
+
|
|
320
|
+
/** Runtime `z.*` facade, merged with the `z.infer` type namespace below. */
|
|
321
|
+
export const z = {
|
|
322
|
+
string,
|
|
323
|
+
number,
|
|
324
|
+
boolean,
|
|
325
|
+
literal,
|
|
326
|
+
enum: enumSchema,
|
|
327
|
+
union,
|
|
328
|
+
array,
|
|
329
|
+
object,
|
|
330
|
+
record,
|
|
331
|
+
unknown,
|
|
332
|
+
any,
|
|
333
|
+
null: nullSchema,
|
|
334
|
+
undefined: undefinedSchema,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export namespace z {
|
|
338
|
+
export type infer<Schema> = Schema extends { readonly _output: infer Out } ? Out : never;
|
|
339
|
+
}
|