@vxnsin/inkan 0.0.0-stage → 0.1.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/dist/router.js ADDED
@@ -0,0 +1,84 @@
1
+ // A segment tree. Static segments win over `:params`, params win over `*rest`,
2
+ // so `/users/me` and `/users/:id` can live side by side in any order.
3
+ const node = () => ({ statics: new Map(), routes: new Map() });
4
+ export function splitPath(path) {
5
+ return path.split("/").filter(Boolean);
6
+ }
7
+ export class Router {
8
+ root = node();
9
+ add(method, path, route) {
10
+ let n = this.root;
11
+ const segments = splitPath(path);
12
+ for (const [i, seg] of segments.entries()) {
13
+ if (seg.startsWith("*")) {
14
+ if (i !== segments.length - 1)
15
+ throw new Error(`A wildcard has to be the last segment: ${path}`);
16
+ n.wildcard ??= { name: seg.slice(1) || "rest", routes: new Map() };
17
+ this.put(n.wildcard.routes, method, path, route);
18
+ return;
19
+ }
20
+ if (seg.startsWith(":")) {
21
+ const name = seg.slice(1);
22
+ if (n.param && n.param.name !== name) {
23
+ throw new Error(`${path} names a parameter :${name} where another route already has :${n.param.name}`);
24
+ }
25
+ n.param ??= { name, node: node() };
26
+ n = n.param.node;
27
+ }
28
+ else {
29
+ let next = n.statics.get(seg);
30
+ if (!next)
31
+ n.statics.set(seg, (next = node()));
32
+ n = next;
33
+ }
34
+ }
35
+ this.put(n.routes, method, path, route);
36
+ }
37
+ put(routes, method, path, route) {
38
+ if (routes.has(method))
39
+ throw new Error(`${method} ${path} is defined twice`);
40
+ routes.set(method, route);
41
+ }
42
+ match(method, path) {
43
+ let segments;
44
+ try {
45
+ segments = splitPath(path).map(decodeURIComponent);
46
+ }
47
+ catch {
48
+ return { kind: "none" };
49
+ }
50
+ const hit = this.walk(this.root, segments, 0, {});
51
+ if (!hit)
52
+ return { kind: "none" };
53
+ const route = hit.routes.get(method) ?? (method === "HEAD" ? hit.routes.get("GET") : undefined);
54
+ if (route)
55
+ return { kind: "found", route, params: hit.params };
56
+ const allow = [...hit.routes.keys()];
57
+ if (allow.includes("GET"))
58
+ allow.push("HEAD");
59
+ return { kind: "method", allow };
60
+ }
61
+ walk(n, segs, i, params) {
62
+ if (i === segs.length) {
63
+ if (n.routes.size)
64
+ return { routes: n.routes, params };
65
+ if (n.wildcard)
66
+ return { routes: n.wildcard.routes, params: { ...params, [n.wildcard.name]: "" } };
67
+ return undefined;
68
+ }
69
+ const s = n.statics.get(segs[i]);
70
+ if (s) {
71
+ const hit = this.walk(s, segs, i + 1, params);
72
+ if (hit)
73
+ return hit;
74
+ }
75
+ if (n.param) {
76
+ const hit = this.walk(n.param.node, segs, i + 1, { ...params, [n.param.name]: segs[i] });
77
+ if (hit)
78
+ return hit;
79
+ }
80
+ if (n.wildcard)
81
+ return { routes: n.wildcard.routes, params: { ...params, [n.wildcard.name]: segs.slice(i).join("/") } };
82
+ return undefined;
83
+ }
84
+ }
@@ -0,0 +1,185 @@
1
+ export type Issue = {
2
+ path: string;
3
+ message: string;
4
+ };
5
+ export type JsonSchema = Record<string, unknown>;
6
+ export type SafeResult<T> = {
7
+ ok: true;
8
+ value: T;
9
+ } | {
10
+ ok: false;
11
+ issues: Issue[];
12
+ };
13
+ export declare class ValidationError extends Error {
14
+ issues: Issue[];
15
+ constructor(issues: Issue[]);
16
+ }
17
+ /** Collects component schemas while a document is written. */
18
+ export type RefContext = {
19
+ components: Map<string, JsonSchema>;
20
+ };
21
+ declare const FAIL: unique symbol;
22
+ type Fail = typeof FAIL;
23
+ type Meta = {
24
+ description?: string;
25
+ examples?: unknown[];
26
+ optional?: boolean;
27
+ nullable?: boolean;
28
+ hasDefault?: boolean;
29
+ default?: unknown;
30
+ name?: string;
31
+ deprecated?: boolean;
32
+ };
33
+ export declare abstract class Schema<T = unknown> {
34
+ readonly _type: T;
35
+ meta: Meta;
36
+ protected abstract check(value: unknown, path: string, coerce: boolean, issues: Issue[]): T | Fail;
37
+ protected abstract json(ctx?: RefContext): JsonSchema;
38
+ /** @internal */
39
+ _run(value: unknown, path: string, coerce: boolean, issues: Issue[]): T | Fail;
40
+ /** @internal */
41
+ _schema(ctx?: RefContext): JsonSchema;
42
+ private decorate;
43
+ protected clone(patch?: Partial<Meta>): this;
44
+ optional(): Schema<T | undefined>;
45
+ nullable(): Schema<T | null>;
46
+ /** The value used when the field is missing. The field becomes optional for callers. */
47
+ default(value: Exclude<T, undefined>): Schema<Exclude<T, undefined>>;
48
+ describe(description: string): this;
49
+ example(...examples: T[]): this;
50
+ deprecated(): this;
51
+ /** Names the schema, so OpenAPI lists it once under components and refers to it. */
52
+ named(name: string): this;
53
+ parse(value: unknown, opts?: {
54
+ coerce?: boolean;
55
+ }): T;
56
+ safeParse(value: unknown, opts?: {
57
+ coerce?: boolean;
58
+ }): SafeResult<T>;
59
+ toJSONSchema(): JsonSchema;
60
+ }
61
+ export type Infer<S> = S extends Schema<infer T> ? T : never;
62
+ export type StringFormat = "email" | "uuid" | "date-time" | "date" | "uri";
63
+ export declare class StringSchema extends Schema<string> {
64
+ private rules;
65
+ min(n: number): this;
66
+ max(n: number): this;
67
+ pattern(re: RegExp): this;
68
+ format(f: StringFormat): this;
69
+ email(): this;
70
+ uuid(): this;
71
+ /** Trims whitespace before the other rules run. */
72
+ trim(): this;
73
+ protected check(v: unknown, path: string, _coerce: boolean, issues: Issue[]): string | typeof FAIL;
74
+ protected json(): JsonSchema;
75
+ }
76
+ export declare class NumberSchema extends Schema<number> {
77
+ private rules;
78
+ constructor(int?: boolean);
79
+ min(n: number): this;
80
+ max(n: number): this;
81
+ positive(): this;
82
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): number | typeof FAIL;
83
+ protected json(): JsonSchema;
84
+ }
85
+ export declare class BooleanSchema extends Schema<boolean> {
86
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): boolean | typeof FAIL;
87
+ protected json(): {
88
+ type: string;
89
+ };
90
+ }
91
+ export declare class EnumSchema<const V extends string | number | boolean> extends Schema<V> {
92
+ values: readonly V[];
93
+ constructor(values: readonly V[]);
94
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): typeof FAIL | V;
95
+ protected json(): {
96
+ const: V;
97
+ enum?: undefined;
98
+ } | {
99
+ const?: undefined;
100
+ enum: V[];
101
+ };
102
+ }
103
+ export declare class AnySchema<T = unknown> extends Schema<T> {
104
+ protected check(v: unknown): T;
105
+ protected json(): {};
106
+ }
107
+ export declare class ArraySchema<S extends Schema<any>> extends Schema<Infer<S>[]> {
108
+ item: S;
109
+ private rules;
110
+ constructor(item: S);
111
+ min(n: number): this;
112
+ max(n: number): this;
113
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): typeof FAIL | Infer<S>[];
114
+ protected json(ctx?: RefContext): JsonSchema;
115
+ }
116
+ export type Shape = Record<string, Schema<any>>;
117
+ type Simplify<T> = {
118
+ [K in keyof T]: T[K];
119
+ } & {};
120
+ type OptionalKeys<S extends Shape> = {
121
+ [K in keyof S]: undefined extends Infer<S[K]> ? K : never;
122
+ }[keyof S];
123
+ export type InferShape<S extends Shape> = Simplify<{
124
+ [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
125
+ } & {
126
+ [K in OptionalKeys<S>]?: Infer<S[K]>;
127
+ }>;
128
+ export declare class ObjectSchema<S extends Shape> extends Schema<InferShape<S>> {
129
+ shape: S;
130
+ private unknownKeys;
131
+ constructor(shape: S);
132
+ /** Unknown keys become an error instead of being dropped. */
133
+ strict(): this;
134
+ /** Unknown keys are kept as they are. */
135
+ passthrough(): this;
136
+ extend<E extends Shape>(more: E): ObjectSchema<Simplify<Omit<S, keyof E> & E>>;
137
+ pick<K extends keyof S>(...keys: K[]): ObjectSchema<Pick<S, K>>;
138
+ omit<K extends keyof S>(...keys: K[]): ObjectSchema<Omit<S, K>>;
139
+ partial(): ObjectSchema<{
140
+ [K in keyof S]: Schema<Infer<S[K]> | undefined>;
141
+ }>;
142
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): typeof FAIL | ({ [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>; } & { [K in OptionalKeys<S>]?: Infer<S[K]> | undefined; } extends infer T ? { [K in keyof T]: T[K]; } : never);
143
+ protected json(ctx?: RefContext): JsonSchema;
144
+ }
145
+ export declare class RecordSchema<S extends Schema<any>> extends Schema<Record<string, Infer<S>>> {
146
+ value: S;
147
+ constructor(value: S);
148
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): typeof FAIL | Record<string, Infer<S>>;
149
+ protected json(ctx?: RefContext): {
150
+ type: string;
151
+ additionalProperties: JsonSchema;
152
+ };
153
+ }
154
+ export declare class UnionSchema<S extends Schema<any>[]> extends Schema<Infer<S[number]>> {
155
+ options: S;
156
+ constructor(options: S);
157
+ protected check(v: unknown, path: string, coerce: boolean, issues: Issue[]): any;
158
+ protected json(ctx?: RefContext): {
159
+ anyOf: JsonSchema[];
160
+ };
161
+ }
162
+ export declare const t: {
163
+ string: () => StringSchema;
164
+ number: () => NumberSchema;
165
+ int: () => NumberSchema;
166
+ boolean: () => BooleanSchema;
167
+ literal: <const V extends string | number | boolean>(value: V) => EnumSchema<V>;
168
+ enum: <const V extends string | number | boolean>(values: readonly V[]) => EnumSchema<V>;
169
+ array: <S extends Schema<any>>(item: S) => ArraySchema<S>;
170
+ object: <S extends Shape>(shape: S) => ObjectSchema<S>;
171
+ record: <S extends Schema<any>>(value: S) => RecordSchema<S>;
172
+ union: <S extends Schema<any>[]>(...options: S) => UnionSchema<S>;
173
+ any: <T = unknown>() => AnySchema<T>;
174
+ /** No body at all, for a 204. */
175
+ empty: () => Schema<undefined>;
176
+ /** An RFC 9457 problem document, the shape every inkan error has. */
177
+ problem: () => ObjectSchema<{
178
+ type: StringSchema;
179
+ title: StringSchema;
180
+ status: NumberSchema;
181
+ detail: Schema<string | undefined>;
182
+ instance: Schema<string | undefined>;
183
+ }>;
184
+ };
185
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,398 @@
1
+ // A small schema builder. One definition gives you a runtime check,
2
+ // a TypeScript type and a JSON Schema, so they cannot drift apart.
3
+ export class ValidationError extends Error {
4
+ issues;
5
+ constructor(issues) {
6
+ super(issues.map((i) => (i.path ? `${i.path}: ${i.message}` : i.message)).join("; "));
7
+ this.name = "ValidationError";
8
+ this.issues = issues;
9
+ }
10
+ }
11
+ const FAIL = Symbol("fail");
12
+ export class Schema {
13
+ meta = {};
14
+ /** @internal */
15
+ _run(value, path, coerce, issues) {
16
+ if (value === undefined) {
17
+ if (this.meta.hasDefault)
18
+ return structuredClone(this.meta.default);
19
+ if (this.meta.optional)
20
+ return undefined;
21
+ issues.push({ path, message: "is required" });
22
+ return FAIL;
23
+ }
24
+ if (value === null && this.meta.nullable)
25
+ return null;
26
+ return this.check(value, path, coerce, issues);
27
+ }
28
+ /** @internal */
29
+ _schema(ctx) {
30
+ if (ctx && this.meta.name) {
31
+ if (!ctx.components.has(this.meta.name)) {
32
+ ctx.components.set(this.meta.name, {}); // reserve first, so recursive shapes terminate
33
+ ctx.components.set(this.meta.name, this.decorate(this.json(ctx)));
34
+ }
35
+ const ref = { $ref: `#/components/schemas/${this.meta.name}` };
36
+ return this.meta.nullable ? { anyOf: [ref, { type: "null" }] } : ref;
37
+ }
38
+ const out = this.decorate(this.json(ctx));
39
+ return this.meta.nullable ? { anyOf: [out, { type: "null" }] } : out;
40
+ }
41
+ decorate(s) {
42
+ const out = { ...s };
43
+ if (this.meta.description)
44
+ out.description = this.meta.description;
45
+ if (this.meta.examples)
46
+ out.examples = this.meta.examples;
47
+ if (this.meta.hasDefault)
48
+ out.default = this.meta.default;
49
+ if (this.meta.deprecated)
50
+ out.deprecated = true;
51
+ return out;
52
+ }
53
+ clone(patch = {}) {
54
+ const copy = Object.create(Object.getPrototypeOf(this));
55
+ Object.assign(copy, this);
56
+ copy.meta = { ...this.meta, ...patch };
57
+ return copy;
58
+ }
59
+ optional() {
60
+ return this.clone({ optional: true });
61
+ }
62
+ nullable() {
63
+ return this.clone({ nullable: true });
64
+ }
65
+ /** The value used when the field is missing. The field becomes optional for callers. */
66
+ default(value) {
67
+ return this.clone({ hasDefault: true, default: value });
68
+ }
69
+ describe(description) {
70
+ return this.clone({ description });
71
+ }
72
+ example(...examples) {
73
+ return this.clone({ examples });
74
+ }
75
+ deprecated() {
76
+ return this.clone({ deprecated: true });
77
+ }
78
+ /** Names the schema, so OpenAPI lists it once under components and refers to it. */
79
+ named(name) {
80
+ return this.clone({ name });
81
+ }
82
+ parse(value, opts = {}) {
83
+ const r = this.safeParse(value, opts);
84
+ if (!r.ok)
85
+ throw new ValidationError(r.issues);
86
+ return r.value;
87
+ }
88
+ safeParse(value, opts = {}) {
89
+ const issues = [];
90
+ const out = this._run(value, "", opts.coerce ?? false, issues);
91
+ if (out === FAIL || issues.length)
92
+ return { ok: false, issues };
93
+ return { ok: true, value: out };
94
+ }
95
+ toJSONSchema() {
96
+ return this._schema();
97
+ }
98
+ }
99
+ const typeOf = (v) => (v === null ? "null" : Array.isArray(v) ? "array" : typeof v);
100
+ // ---------- primitives ----------
101
+ const FORMATS = {
102
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
103
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
104
+ "date-time": /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/,
105
+ date: /^\d{4}-\d{2}-\d{2}$/,
106
+ uri: /^[a-z][a-z0-9+.-]*:\/\/\S+$/i,
107
+ };
108
+ export class StringSchema extends Schema {
109
+ rules = {};
110
+ min(n) { const c = this.clone(); c.rules = { ...this.rules, min: n }; return c; }
111
+ max(n) { const c = this.clone(); c.rules = { ...this.rules, max: n }; return c; }
112
+ pattern(re) { const c = this.clone(); c.rules = { ...this.rules, pattern: re }; return c; }
113
+ format(f) { const c = this.clone(); c.rules = { ...this.rules, format: f }; return c; }
114
+ email() { return this.format("email"); }
115
+ uuid() { return this.format("uuid"); }
116
+ /** Trims whitespace before the other rules run. */
117
+ trim() { const c = this.clone(); c.rules = { ...this.rules, trim: true }; return c; }
118
+ check(v, path, _coerce, issues) {
119
+ if (typeof v !== "string") {
120
+ issues.push({ path, message: `expected a string, got ${typeOf(v)}` });
121
+ return FAIL;
122
+ }
123
+ const s = this.rules.trim ? v.trim() : v;
124
+ const { min, max, pattern, format } = this.rules;
125
+ if (min !== undefined && s.length < min)
126
+ issues.push({ path, message: `must be at least ${min} characters` });
127
+ if (max !== undefined && s.length > max)
128
+ issues.push({ path, message: `must be at most ${max} characters` });
129
+ if (pattern && !pattern.test(s))
130
+ issues.push({ path, message: `must match ${pattern}` });
131
+ if (format && !FORMATS[format].test(s))
132
+ issues.push({ path, message: `must be a valid ${format}` });
133
+ return s;
134
+ }
135
+ json() {
136
+ const { min, max, pattern, format } = this.rules;
137
+ const s = { type: "string" };
138
+ if (min !== undefined)
139
+ s.minLength = min;
140
+ if (max !== undefined)
141
+ s.maxLength = max;
142
+ if (pattern)
143
+ s.pattern = pattern.source;
144
+ if (format)
145
+ s.format = format;
146
+ return s;
147
+ }
148
+ }
149
+ export class NumberSchema extends Schema {
150
+ rules = {};
151
+ constructor(int = false) {
152
+ super();
153
+ this.rules.int = int;
154
+ }
155
+ min(n) { const c = this.clone(); c.rules = { ...this.rules, min: n }; return c; }
156
+ max(n) { const c = this.clone(); c.rules = { ...this.rules, max: n }; return c; }
157
+ positive() { return this.min(this.rules.int ? 1 : Number.MIN_VALUE); }
158
+ check(v, path, coerce, issues) {
159
+ let n = v;
160
+ if (coerce && typeof v === "string" && v.trim() !== "")
161
+ n = Number(v);
162
+ const kind = this.rules.int ? "an integer" : "a number";
163
+ if (typeof n !== "number" || Number.isNaN(n) || !Number.isFinite(n)) {
164
+ issues.push({ path, message: `expected ${kind}, got ${typeof v === "string" ? JSON.stringify(v) : typeOf(v)}` });
165
+ return FAIL;
166
+ }
167
+ if (this.rules.int && !Number.isInteger(n)) {
168
+ issues.push({ path, message: `expected an integer, got ${n}` });
169
+ return FAIL;
170
+ }
171
+ const { min, max } = this.rules;
172
+ if (min !== undefined && n < min)
173
+ issues.push({ path, message: `must be ${min} or more` });
174
+ if (max !== undefined && n > max)
175
+ issues.push({ path, message: `must be ${max} or less` });
176
+ return n;
177
+ }
178
+ json() {
179
+ const s = { type: this.rules.int ? "integer" : "number" };
180
+ if (this.rules.min !== undefined)
181
+ s.minimum = this.rules.min;
182
+ if (this.rules.max !== undefined)
183
+ s.maximum = this.rules.max;
184
+ return s;
185
+ }
186
+ }
187
+ export class BooleanSchema extends Schema {
188
+ check(v, path, coerce, issues) {
189
+ if (typeof v === "boolean")
190
+ return v;
191
+ if (coerce && typeof v === "string") {
192
+ if (v === "true" || v === "1" || v === "")
193
+ return true;
194
+ if (v === "false" || v === "0")
195
+ return false;
196
+ }
197
+ issues.push({ path, message: `expected a boolean, got ${typeOf(v)}` });
198
+ return FAIL;
199
+ }
200
+ json() {
201
+ return { type: "boolean" };
202
+ }
203
+ }
204
+ export class EnumSchema extends Schema {
205
+ values;
206
+ constructor(values) {
207
+ super();
208
+ this.values = values;
209
+ }
210
+ check(v, path, coerce, issues) {
211
+ const hit = this.values.find((x) => x === v || (coerce && typeof v === "string" && String(x) === v));
212
+ if (hit === undefined) {
213
+ issues.push({ path, message: `must be one of ${this.values.map((x) => JSON.stringify(x)).join(", ")}` });
214
+ return FAIL;
215
+ }
216
+ return hit;
217
+ }
218
+ json() {
219
+ return this.values.length === 1 ? { const: this.values[0] } : { enum: [...this.values] };
220
+ }
221
+ }
222
+ export class AnySchema extends Schema {
223
+ check(v) {
224
+ return v;
225
+ }
226
+ json() {
227
+ return {};
228
+ }
229
+ }
230
+ // ---------- composites ----------
231
+ export class ArraySchema extends Schema {
232
+ item;
233
+ rules = {};
234
+ constructor(item) {
235
+ super();
236
+ this.item = item;
237
+ }
238
+ min(n) { const c = this.clone(); c.rules = { ...this.rules, min: n }; return c; }
239
+ max(n) { const c = this.clone(); c.rules = { ...this.rules, max: n }; return c; }
240
+ check(v, path, coerce, issues) {
241
+ // ?tag=a reads as "a", ?tag=a&tag=b as ["a","b"]: both are a list here.
242
+ const list = coerce && !Array.isArray(v) ? [v] : v;
243
+ if (!Array.isArray(list)) {
244
+ issues.push({ path, message: `expected an array, got ${typeOf(v)}` });
245
+ return FAIL;
246
+ }
247
+ const { min, max } = this.rules;
248
+ if (min !== undefined && list.length < min)
249
+ issues.push({ path, message: `must have at least ${min} items` });
250
+ if (max !== undefined && list.length > max)
251
+ issues.push({ path, message: `must have at most ${max} items` });
252
+ const out = [];
253
+ list.forEach((x, i) => {
254
+ const r = this.item._run(x, `${path}[${i}]`, coerce, issues);
255
+ if (r !== FAIL)
256
+ out.push(r);
257
+ });
258
+ return out;
259
+ }
260
+ json(ctx) {
261
+ const s = { type: "array", items: this.item._schema(ctx) };
262
+ if (this.rules.min !== undefined)
263
+ s.minItems = this.rules.min;
264
+ if (this.rules.max !== undefined)
265
+ s.maxItems = this.rules.max;
266
+ return s;
267
+ }
268
+ }
269
+ export class ObjectSchema extends Schema {
270
+ shape;
271
+ unknownKeys = "strip";
272
+ constructor(shape) {
273
+ super();
274
+ this.shape = shape;
275
+ }
276
+ /** Unknown keys become an error instead of being dropped. */
277
+ strict() { const c = this.clone(); c.unknownKeys = "strict"; return c; }
278
+ /** Unknown keys are kept as they are. */
279
+ passthrough() { const c = this.clone(); c.unknownKeys = "keep"; return c; }
280
+ extend(more) {
281
+ return new ObjectSchema({ ...this.shape, ...more });
282
+ }
283
+ pick(...keys) {
284
+ return new ObjectSchema(Object.fromEntries(keys.map((k) => [k, this.shape[k]])));
285
+ }
286
+ omit(...keys) {
287
+ const s = { ...this.shape };
288
+ for (const k of keys)
289
+ delete s[k];
290
+ return new ObjectSchema(s);
291
+ }
292
+ partial() {
293
+ return new ObjectSchema(Object.fromEntries(Object.entries(this.shape).map(([k, s]) => [k, s.optional()])));
294
+ }
295
+ check(v, path, coerce, issues) {
296
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
297
+ issues.push({ path, message: `expected an object, got ${typeOf(v)}` });
298
+ return FAIL;
299
+ }
300
+ const input = v;
301
+ const out = this.unknownKeys === "keep" ? { ...input } : {};
302
+ for (const [key, schema] of Object.entries(this.shape)) {
303
+ const r = schema._run(input[key], path ? `${path}.${key}` : key, coerce, issues);
304
+ if (r !== FAIL && r !== undefined)
305
+ out[key] = r;
306
+ }
307
+ if (this.unknownKeys === "strict") {
308
+ for (const key of Object.keys(input)) {
309
+ if (!(key in this.shape))
310
+ issues.push({ path: path ? `${path}.${key}` : key, message: "is not allowed" });
311
+ }
312
+ }
313
+ return out;
314
+ }
315
+ json(ctx) {
316
+ const properties = {};
317
+ const required = [];
318
+ for (const [key, schema] of Object.entries(this.shape)) {
319
+ properties[key] = schema._schema(ctx);
320
+ if (!schema.meta.optional && !schema.meta.hasDefault)
321
+ required.push(key);
322
+ }
323
+ const s = { type: "object", properties };
324
+ if (required.length)
325
+ s.required = required;
326
+ if (this.unknownKeys === "strict")
327
+ s.additionalProperties = false;
328
+ return s;
329
+ }
330
+ }
331
+ export class RecordSchema extends Schema {
332
+ value;
333
+ constructor(value) {
334
+ super();
335
+ this.value = value;
336
+ }
337
+ check(v, path, coerce, issues) {
338
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
339
+ issues.push({ path, message: `expected an object, got ${typeOf(v)}` });
340
+ return FAIL;
341
+ }
342
+ const out = {};
343
+ for (const [k, x] of Object.entries(v)) {
344
+ const r = this.value._run(x, path ? `${path}.${k}` : k, coerce, issues);
345
+ if (r !== FAIL)
346
+ out[k] = r;
347
+ }
348
+ return out;
349
+ }
350
+ json(ctx) {
351
+ return { type: "object", additionalProperties: this.value._schema(ctx) };
352
+ }
353
+ }
354
+ export class UnionSchema extends Schema {
355
+ options;
356
+ constructor(options) {
357
+ super();
358
+ this.options = options;
359
+ }
360
+ check(v, path, coerce, issues) {
361
+ for (const option of this.options) {
362
+ const local = [];
363
+ const r = option._run(v, path, coerce, local);
364
+ if (r !== FAIL && local.length === 0)
365
+ return r;
366
+ }
367
+ issues.push({ path, message: "matches none of the allowed shapes" });
368
+ return FAIL;
369
+ }
370
+ json(ctx) {
371
+ return { anyOf: this.options.map((o) => o._schema(ctx)) };
372
+ }
373
+ }
374
+ // ---------- the builder ----------
375
+ const problemShape = {
376
+ type: new StringSchema().describe("A stable code for this kind of problem"),
377
+ title: new StringSchema(),
378
+ status: new NumberSchema(true),
379
+ detail: new StringSchema().optional(),
380
+ instance: new StringSchema().optional(),
381
+ };
382
+ export const t = {
383
+ string: () => new StringSchema(),
384
+ number: () => new NumberSchema(false),
385
+ int: () => new NumberSchema(true),
386
+ boolean: () => new BooleanSchema(),
387
+ literal: (value) => new EnumSchema([value]),
388
+ enum: (values) => new EnumSchema(values),
389
+ array: (item) => new ArraySchema(item),
390
+ object: (shape) => new ObjectSchema(shape),
391
+ record: (value) => new RecordSchema(value),
392
+ union: (...options) => new UnionSchema(options),
393
+ any: () => new AnySchema(),
394
+ /** No body at all, for a 204. */
395
+ empty: () => new AnySchema().optional().describe("No content"),
396
+ /** An RFC 9457 problem document, the shape every inkan error has. */
397
+ problem: () => new ObjectSchema(problemShape).passthrough().named("Problem").describe("An RFC 9457 problem document"),
398
+ };