typespec-http-zod 0.30.0 → 0.32.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.
@@ -37,6 +37,11 @@ export declare function routeTemplateOf(uriTemplate: string, parameters: readonl
37
37
  };
38
38
  /** What a value decodes into: one value, a list, or name/value pairs (a record or a model). */
39
39
  export type ExpansionShape = "scalar" | "list" | "record";
40
+ /**
41
+ * What a decoder reads an expansion as: one of those, or `any` - a value of any type, which is the
42
+ * shape its expansion spells. See {@link URI_EXPANSION_SOURCE}.
43
+ */
44
+ export type DecodedShape = ExpansionShape | "any";
40
45
  /**
41
46
  * Whether a path value needs decoding at all. **An ordinary whole-segment parameter does not**, and
42
47
  * leaving its validator untouched keeps every spec without an RFC 6570 form byte-identical.
@@ -54,9 +59,73 @@ export declare function uriExpansionCall(spec: {
54
59
  readonly operator: string;
55
60
  readonly name: string;
56
61
  readonly explode: boolean;
57
- readonly shape: ExpansionShape;
58
- readonly values: "number" | "boolean" | "";
62
+ readonly shape: DecodedShape;
63
+ readonly readers: ValueReaders;
59
64
  }): string;
65
+ /**
66
+ * **The schemas a record's or a model's values are read against**, as Zod expressions: one for every
67
+ * value of a record, or one per key of a model. `null` where no value can be anything but text, so
68
+ * a service whose parameters are all strings declares no reader.
69
+ */
70
+ export interface ValueReaders {
71
+ readonly values: string | null;
72
+ readonly fields: Readonly<Record<string, string>> | null;
73
+ /**
74
+ * **A discriminated base's readers per subtype**, chosen by the value's own discriminator: a header
75
+ * `Pet` carrying `kind,cat,x,5` reads `x` as `Cat` declares it, a string, where `Dog`'s `x` is a
76
+ * number. Read by one table for every subtype, the first subtype's reading won.
77
+ */
78
+ readonly by?: {
79
+ readonly property: string;
80
+ readonly readers: Readonly<Record<string, ValueReaders>>;
81
+ };
82
+ }
83
+ /**
84
+ * A reader for a list: a lone value boxed into the list it stands for, each element read against the
85
+ * element's schema - `?tags=1&tags=2` gathered as `["1", "2"]`, and `?tags=1` as `["1"]`.
86
+ */
87
+ export declare function eachReader(element: string | readonly string[], one?: string, whole?: string): string;
88
+ export declare const NO_READERS: ValueReaders;
89
+ /**
90
+ * **Wire text read as the JSON value it spells, as its schema says it is.** A path, query, header,
91
+ * cookie or form value is always text, and the document's schema describes it once read: `"5"` is
92
+ * `5` where the schema is `int32`, `true` where it is `boolean`, and stays `"5"` where it is a
93
+ * string. The text is tried as a number, then as a boolean, then as itself, and the first the schema
94
+ * accepts is the value - so `limit: int32 | "all"` reads `"5"` as `5` and `"all"` as `"all"`, and
95
+ * `int32 | boolean` reads both. Where none is accepted the first reading is handed on, to fail with
96
+ * the error that reading earns.
97
+ *
98
+ * **A number is text in JSON's number grammar, and nothing looser.** Read by `Number()`, `"0x10"`,
99
+ * `"007"` and `" 5"` were numbers: `int32` accepted text no JSON number is written as, and `"007" |
100
+ * int32` turned the `"007"` it accepts into `7`.
101
+ *
102
+ * Read by a kind instead - number, boolean or text, decided from the type - a numeric enum, a union
103
+ * of a number and a boolean, and a number beside a string literal were never read at all.
104
+ */
105
+ export declare const WIRE_READ_SOURCE = "\n/** Wire text as the JSON value it spells that the schema accepts: a number, a boolean, or the text. */\nfunction wireRead(raw: unknown, schema: z.ZodType): unknown {\n\tif (typeof raw !== \"string\") return raw;\n\tconst readings: unknown[] = [];\n\tif (/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/.test(raw) && Number.isFinite(Number(raw))) {\n\t\treadings.push(Number(raw));\n\t}\n\tif (raw === \"true\" || raw === \"false\") readings.push(raw === \"true\");\n\tfor (const reading of [...readings, raw]) if (schema.safeParse(reading).success) return reading;\n\treturn readings[0] ?? raw;\n}\n";
106
+ /**
107
+ * How one value is read: against a schema, or as a list boxed and read element by element - a
108
+ * record's or a model's value, through the decoders below, and a multipart text part that may be one
109
+ * value or several. Declared beside its callers only.
110
+ */
111
+ export declare const WIRE_READ_AS_SOURCE = "\n/** How one value is read: against a schema, or as a list of values each read against one. */\ntype WireReader =\n\t| z.ZodType\n\t| { readonly each: z.ZodType | readonly z.ZodType[]; readonly one?: z.ZodType; readonly whole?: z.ZodType };\n\n/** A value read against its reader: one value, or a list of them. */\nfunction wireReadAs(value: unknown, reader: WireReader | null | undefined): unknown {\n\tif (reader === null || reader === undefined) return value;\n\tif (\"each\" in reader) {\n\t\tconst accepted = (read: unknown): boolean => reader.whole === undefined || reader.whole.safeParse(read).success;\n\t\t// A value that may be one value or a list of them: a lone occurrence is the one value, where that is one.\n\t\tif (!Array.isArray(value) && reader.one !== undefined) {\n\t\t\tconst one = wireRead(value, reader.one);\n\t\t\tif (accepted(one)) return one;\n\t\t}\n\t\t// A list read as the first of its list members' elements the whole accepts: `int32[] | string[]`.\n\t\tconst items: unknown[] = Array.isArray(value) ? value : [value];\n\t\tconst lists: readonly z.ZodType[] = Array.isArray(reader.each) ? reader.each : [reader.each as z.ZodType];\n\t\tconst readings = lists.map((each) => items.map((item) => wireRead(item, each)));\n\t\treturn readings.find(accepted) ?? readings[0];\n\t}\n\treturn wireRead(value, reader);\n}\n";
112
+ /**
113
+ * How the RFC 6570 decoders read a record's or a model's values: by the reader for the key, looked
114
+ * up as the table's OWN property - a key named `constructor` found `Object` on the prototype and
115
+ * handed it to `wireRead`, which threw - each read by {@link WIRE_READ_AS_SOURCE}. Declared beside
116
+ * those decoders only, which are its only callers.
117
+ */
118
+ export declare const WIRE_READERS_SOURCE = "\n/** A record's or a model's readers: every value's, each key's, and a discriminated base's per subtype. */\ntype WireReaders = {\n\treadonly values: WireReader | null;\n\treadonly fields: globalThis.Readonly<globalThis.Record<string, WireReader>> | null;\n\treadonly by?: { readonly property: string; readonly readers: globalThis.Readonly<globalThis.Record<string, WireReaders>> };\n};\n\n/** The readers for an object whose discriminator reads `kind`: the subtype it names, else the base's. */\nfunction wireReadersOf(spec: WireReaders, kind: unknown): WireReaders {\n\tif (spec.by === undefined || typeof kind !== \"string\") return spec;\n\treturn Object.prototype.hasOwnProperty.call(spec.by.readers, kind) ? (spec.by.readers[kind] ?? spec) : spec;\n}\n\n/** The value an object's discriminator holds, as its own property. */\nfunction wireKindOf(spec: WireReaders, raw: object): unknown {\n\treturn spec.by !== undefined && Object.prototype.hasOwnProperty.call(raw, spec.by.property)\n\t\t? (raw as globalThis.Record<string, unknown>)[spec.by.property]\n\t\t: undefined;\n}\n\n/** A model's reader for a key it declares, read as its own property only, or a record's for any. */\nfunction wireReaderOf(spec: WireReaders, key: string): WireReader | null {\n\tconst own = spec.fields !== null && Object.prototype.hasOwnProperty.call(spec.fields, key);\n\treturn (own ? spec.fields?.[key] : spec.values) ?? null;\n}\n";
119
+ /**
120
+ * **A gathered object read as one member of a union of models reads it.** A form-exploded query
121
+ * typed `A | B` arrives as one set of keys, gathered once under the parameter's name; each member then
122
+ * reads the values as its own properties say. Gathered once per member, the last member's gatherer
123
+ * claimed the name and every other member's keys stayed unread, so `?kind=a&x=1` was refused.
124
+ * Copied key by key as own properties, so a key named `__proto__` is a key and not the prototype.
125
+ */
126
+ export declare const WIRE_READ_OBJECT_SOURCE = "\n/** A gathered object's values read against one member's readers. */\nfunction wireReadObject(raw: unknown, spec: WireReaders): unknown {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) return raw;\n\tconst readers = wireReadersOf(spec, wireKindOf(spec, raw));\n\tconst read: globalThis.Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(raw)) {\n\t\tObject.defineProperty(read, key, { value: wireReadAs(value, wireReaderOf(readers, key)), enumerable: true, writable: true, configurable: true });\n\t}\n\treturn read;\n}\n";
127
+ /** The call that reads a gathered object as one member of a union - see {@link WIRE_READ_OBJECT_SOURCE}. */
128
+ export declare function wireReadObjectCall(readers: ValueReaders): string;
60
129
  /**
61
130
  * **RFC 6570 section 3.2, run backwards.** A router hands over the text of the segment an expression
62
131
  * sits in (or, for an exploding `/` expression, every segment after its literal), and this undoes
@@ -68,8 +137,16 @@ export declare function uriExpansionCall(spec: {
68
137
  * the refusal to the schema admitted a request the route does not match. A list's element values
69
138
  * are decoded by the element decoder inside, as a query list's are; a record's values are decoded
70
139
  * here, because no schema expression reaches them one by one.
140
+ *
141
+ * **A value of any type is read as the shape its expansion spells** (`any`): `unknown`, a scalar
142
+ * with no standard base, or a union holding one, which openapi3 publishes as `{}` or with `{}` among
143
+ * its members. Under `{;v*}` a scalar is exactly one `v=` part, so `;v=1;v=2` - `[1, 2]` in the
144
+ * document - and `;a=1;b=2` - `{ a: 1, b: 2 }` - had no reading, and were refused as not the
145
+ * expansion the route declares. Several parts all named `v` are a list, any other an object, each
146
+ * value read as the text it spells; anywhere else, and as one `v=` part, every expansion is also a
147
+ * scalar's, which is the reading taken first, as `wireRead` takes the number first.
71
148
  */
72
- export declare const URI_EXPANSION_SOURCE = "\n/** RFC 6570 section 3.2 in reverse: one expression's expansion, decoded into the value it expanded. */\nfunction uriExpansion(\n\traw: unknown,\n\tctx: { addIssue(issue: { code: \"custom\"; message: string }): void },\n\tspec: {\n\t\treadonly prefix: string;\n\t\treadonly suffix: string;\n\t\treadonly operator: string;\n\t\treadonly name: string;\n\t\treadonly explode: boolean;\n\t\treadonly shape: \"scalar\" | \"list\" | \"record\";\n\t\treadonly values: \"number\" | \"boolean\" | \"\";\n\t},\n): unknown {\n\tif (typeof raw !== \"string\") return raw;\n\tconst refuse = (): unknown => {\n\t\tctx.addIssue({ code: \"custom\", message: `Not the expansion of {${spec.operator}${spec.name}${spec.explode ? \"*\" : \"\"}} this route declares` });\n\t\treturn z.NEVER;\n\t};\n\tif (raw.length < spec.prefix.length + spec.suffix.length) return refuse();\n\tif (!raw.startsWith(spec.prefix) || !raw.endsWith(spec.suffix)) return refuse();\n\tlet text = raw.slice(spec.prefix.length, raw.length - spec.suffix.length);\n\tif (spec.operator === \".\" || spec.operator === \";\") {\n\t\tif (!text.startsWith(spec.operator)) return refuse();\n\t\ttext = text.slice(1);\n\t}\n\tif (spec.operator === \";\" && !(spec.shape === \"record\" && spec.explode)) {\n\t\tconst named = text\n\t\t\t.split(\";\")\n\t\t\t.map((part) => (part === spec.name ? \"\" : part.startsWith(`${spec.name}=`) ? part.slice(spec.name.length + 1) : undefined));\n\t\tif (named.some((part) => part === undefined)) return refuse();\n\t\tif (spec.shape === \"list\" && spec.explode) return named;\n\t\tif (named.length !== 1) return refuse();\n\t\ttext = named[0] ?? \"\";\n\t}\n\tif (spec.shape === \"scalar\") return text;\n\tconst joiner = spec.explode && (spec.operator === \".\" || spec.operator === \"/\" || spec.operator === \";\") ? spec.operator : \",\";\n\tif (spec.shape === \"list\") return text === \"\" ? [] : text.split(joiner);\n\tconst value = (item: string): unknown =>\n\t\tspec.values === \"number\" && item.trim() !== \"\" && Number.isFinite(Number(item))\n\t\t\t? Number(item)\n\t\t\t: spec.values === \"boolean\" && (item === \"true\" || item === \"false\")\n\t\t\t\t? item === \"true\"\n\t\t\t\t: item;\n\tconst entries: [string, unknown][] = [];\n\tif (spec.explode) {\n\t\tfor (const pair of text === \"\" ? [] : text.split(joiner)) {\n\t\t\tconst at = pair.indexOf(\"=\");\n\t\t\tif (at === -1) return refuse();\n\t\t\tentries.push([pair.slice(0, at), value(pair.slice(at + 1))]);\n\t\t}\n\t} else {\n\t\tconst items = text === \"\" ? [] : text.split(\",\");\n\t\tif (items.length % 2 !== 0) return refuse();\n\t\tfor (let index = 0; index < items.length; index += 2) {\n\t\t\tentries.push([items[index] ?? \"\", value(items[index + 1] ?? \"\")]);\n\t\t}\n\t}\n\treturn Object.fromEntries(entries);\n}\n";
149
+ export declare const URI_EXPANSION_SOURCE = "\n/** RFC 6570 section 3.2 in reverse: one expression's expansion, decoded into the value it expanded. */\nfunction uriExpansion(\n\traw: unknown,\n\tctx: { addIssue(issue: { code: \"custom\"; message: string }): void },\n\tspec: WireReaders & {\n\t\treadonly prefix: string;\n\t\treadonly suffix: string;\n\t\treadonly operator: string;\n\t\treadonly name: string;\n\t\treadonly explode: boolean;\n\t\treadonly shape: \"scalar\" | \"list\" | \"record\" | \"any\";\n\t},\n): unknown {\n\tif (typeof raw !== \"string\") return raw;\n\tconst refuse = (): unknown => {\n\t\tctx.addIssue({ code: \"custom\", message: `Not the expansion of {${spec.operator}${spec.name}${spec.explode ? \"*\" : \"\"}} this route declares` });\n\t\treturn z.NEVER;\n\t};\n\tif (raw.length < spec.prefix.length + spec.suffix.length) return refuse();\n\tif (!raw.startsWith(spec.prefix) || !raw.endsWith(spec.suffix)) return refuse();\n\tlet text = raw.slice(spec.prefix.length, raw.length - spec.suffix.length);\n\tif (spec.operator === \".\" || spec.operator === \";\") {\n\t\tif (!text.startsWith(spec.operator)) return refuse();\n\t\ttext = text.slice(1);\n\t}\n\tconst parts = text.split(\";\");\n\tconst own = parts.filter((part) => part === spec.name || part.startsWith(`${spec.name}=`)).length;\n\t// A value of any type is the shape its expansion spells: only `{;v*}` spells a list or an object no scalar does.\n\tconst shape =\n\t\tspec.shape !== \"any\"\n\t\t\t? spec.shape\n\t\t\t: spec.operator !== \";\" || !spec.explode || text === \"\" || (parts.length === 1 && own === 1)\n\t\t\t\t? \"scalar\"\n\t\t\t\t: own === parts.length\n\t\t\t\t\t? \"list\"\n\t\t\t\t\t: \"record\";\n\tif (spec.operator === \";\" && !(shape === \"record\" && spec.explode)) {\n\t\tconst named = parts.map((part) => (part === spec.name ? \"\" : part.startsWith(`${spec.name}=`) ? part.slice(spec.name.length + 1) : undefined));\n\t\tif (named.some((part) => part === undefined)) return refuse();\n\t\tif (shape === \"list\" && spec.explode) {\n\t\t\t// Read here where no element decoder runs after: the elements of a value of any type.\n\t\t\treturn spec.shape === \"any\" ? named.map((item) => wireReadAs(item, spec.values)) : named;\n\t\t}\n\t\tif (named.length !== 1) return refuse();\n\t\ttext = named[0] ?? \"\";\n\t}\n\tif (shape === \"scalar\") return text;\n\tconst joiner = spec.explode && (spec.operator === \".\" || spec.operator === \"/\" || spec.operator === \";\") ? spec.operator : \",\";\n\tif (shape === \"list\") return text === \"\" ? [] : text.split(joiner);\n\tconst entries: [string, string][] = [];\n\tif (spec.explode) {\n\t\tfor (const piece of text === \"\" ? [] : text.split(joiner)) {\n\t\t\tconst at = piece.indexOf(\"=\");\n\t\t\tconst last = entries[entries.length - 1];\n\t\t\t// A value holding the joiner, which the expansion leaves as it is - `a=1.5` under `{.r*}` - goes on.\n\t\t\tif (at === -1 && last !== undefined) last[1] += `${joiner}${piece}`;\n\t\t\telse if (at === -1) return refuse();\n\t\t\telse entries.push([piece.slice(0, at), piece.slice(at + 1)]);\n\t\t}\n\t} else {\n\t\tconst items = text === \"\" ? [] : text.split(\",\");\n\t\tif (items.length % 2 !== 0) return refuse();\n\t\tfor (let index = 0; index < items.length; index += 2) {\n\t\t\tentries.push([items[index] ?? \"\", items[index + 1] ?? \"\"]);\n\t\t}\n\t}\n\tconst readers = wireReadersOf(spec, wireKindOf(spec, Object.fromEntries(entries)));\n\treturn Object.fromEntries(entries.map(([key, item]) => [key, wireReadAs(item, wireReaderOf(readers, key))]));\n}\n";
73
150
  /**
74
151
  * The call that gathers a form-exploded record or model back under its parameter name. The
75
152
  * gatherer itself is {@link EXPLODED_QUERY_SOURCE}.
@@ -80,15 +157,40 @@ export declare function explodedQueryCall(spec: {
80
157
  readonly others: readonly string[];
81
158
  /** For a model, its own properties' wire names; `undefined` for a record. */
82
159
  readonly keys: readonly string[] | undefined;
83
- readonly numbers: readonly string[];
84
- readonly booleans: readonly string[];
85
- /** For a record, how every value decodes. */
86
- readonly values: "number" | "boolean" | "";
160
+ /**
161
+ * Whether the parameter may also be sent as itself - a list or a scalar beside its records and
162
+ * models - so what arrived under its name is kept for those, see {@link WIRE_ITSELF_SOURCE}.
163
+ */
164
+ readonly itself: boolean;
165
+ readonly readers: ValueReaders;
87
166
  }): string;
88
167
  /**
89
168
  * **`{?param*}` over a record or a model expands to the object's own keys**, `?a=1&b=2` and
90
169
  * `?field=status&value=active`, so there is no `param` key for a query validator to find. This
91
170
  * gathers those keys back under the parameter's name before the query object is validated: a model
92
171
  * takes exactly its declared properties, a record every key no other query parameter claims.
172
+ *
173
+ * **The parameter's own name is a key like any other, and only that.** `?f=5` beside `f?: F`, where
174
+ * `F` declares no `f`, is no key of the object and no other parameter's: the document reads it as a
175
+ * parameter it does not declare, and accepts it. Left under the name, it reached the model's schema
176
+ * as the string `"5"`, and was refused, where `?zzz=5` was accepted. Only a parameter that may also be
177
+ * sent as itself keeps it - see {@link WIRE_ITSELF_SOURCE}.
178
+ */
179
+ export declare const EXPLODED_QUERY_SOURCE = "\n/** Each object a gatherer made for a parameter that may also be sent as itself, and what was: one value, or none. */\nconst wireSentAsItself = new WeakMap<object, readonly unknown[]>();\n\n/** RFC 6570 form expansion of an exploded record or model, gathered back under its parameter. */\nfunction explodedQuery(\n\traw: unknown,\n\tspec: WireReaders & {\n\t\treadonly name: string;\n\t\treadonly others: readonly string[];\n\t\treadonly keys: readonly string[] | null;\n\t\treadonly itself: boolean;\n\t},\n): unknown {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) return raw;\n\tconst readers = wireReadersOf(spec, wireKindOf(spec, raw));\n\tconst gathered: globalThis.Record<string, unknown> = {};\n\tconst rest: globalThis.Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(raw)) {\n\t\t// Already gathered only where an OBJECT sits under the name: a query value from the wire is a\n\t\t// string or a list of them, and `{ param: 0 }` expands to `?param=0`.\n\t\tif (key === spec.name && value !== null && typeof value === \"object\" && !Array.isArray(value)) return raw;\n\t\t/**\n\t\t * Left in place as well, for every other object gathered from the same query: `f: F1` and\n\t\t * `g: F2` both declaring `a` each read `?a=1`, as the document reads it, where the first\n\t\t * gatherer took `a` and the second refused a missing `g`. The query object strips it after.\n\t\t */\n\t\trest[key] = value;\n\t\t// A key another query parameter is named is that parameter's, the reading the document takes.\n\t\tconst own = !spec.others.includes(key) && (spec.keys === null || spec.keys.includes(key));\n\t\tif (own) gathered[key] = wireReadAs(value, wireReaderOf(readers, key));\n\t}\n\tconst sent = Object.prototype.hasOwnProperty.call(raw, spec.name);\n\tif (Object.keys(gathered).length > 0) {\n\t\tif (spec.itself) wireSentAsItself.set(gathered, sent ? [(raw as globalThis.Record<string, unknown>)[spec.name]] : []);\n\t\treturn { ...rest, [spec.name]: gathered };\n\t}\n\t// Nothing gathered: the name sent alone is the parameter itself, where it may be, or a key nothing declares.\n\tif (!sent || spec.itself) return raw;\n\tdelete rest[spec.name];\n\treturn rest;\n}\n";
180
+ /** The call that hands a list or a scalar form the value its parameter was sent as itself. */
181
+ export declare function wireItselfCall(): string;
182
+ /**
183
+ * **A parameter that may be an object or a list is read both ways from one query.** `x?: int32[] |
184
+ * Record<int32>` exploded is `?x=1&x=2` as the list and `?a=1&b=2` as the record - and `?x=5` as
185
+ * either, since `{ x: 5 }` expands to it too. The record's gatherer takes every key, the parameter's
186
+ * own name included, so the list member was handed `{ x: ["1", "2"] }` and refused it: `?x=1&x=2`,
187
+ * `[1, 2]` in the document, was refused for every such union, and `?x=007`, `["007"]`, for
188
+ * `string[] | Record<int32>`. The gatherer now records what arrived under the name beside the object
189
+ * it made, and each member that is not an object reads that, while each that is reads the object -
190
+ * so the union's first member the value satisfies is its reading, as everywhere else.
191
+ *
192
+ * **Only an object the gatherer made carries it.** One a caller built - the value already gathered,
193
+ * or `queryFields` given structured input - is the object it is, and a member that is not an object
194
+ * refuses it as before; the gatherer's own object, with nothing sent under the name, is refused here.
93
195
  */
94
- export declare const EXPLODED_QUERY_SOURCE = "\n/** RFC 6570 form expansion of an exploded record or model, gathered back under its parameter. */\nfunction explodedQuery(\n\traw: unknown,\n\tspec: {\n\t\treadonly name: string;\n\t\treadonly others: readonly string[];\n\t\treadonly keys: readonly string[] | null;\n\t\treadonly numbers: readonly string[];\n\t\treadonly booleans: readonly string[];\n\t\treadonly values: \"number\" | \"boolean\" | \"\";\n\t},\n): unknown {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) return raw;\n\tconst gathered: Record<string, unknown> = {};\n\tconst rest: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(raw)) {\n\t\t// Already gathered only where an OBJECT sits under the name: a query value from the wire is a\n\t\t// string or a list of them, and `{ param: 0 }` expands to `?param=0`.\n\t\tif (key === spec.name && value !== null && typeof value === \"object\" && !Array.isArray(value)) return raw;\n\t\tconst own = spec.keys === null ? !spec.others.includes(key) : spec.keys.includes(key);\n\t\tif (!own) {\n\t\t\trest[key] = value;\n\t\t\tcontinue;\n\t\t}\n\t\tconst kind = spec.values !== \"\" ? spec.values : spec.numbers.includes(key) ? \"number\" : spec.booleans.includes(key) ? \"boolean\" : \"\";\n\t\tgathered[key] =\n\t\t\ttypeof value !== \"string\"\n\t\t\t\t? value\n\t\t\t\t: kind === \"number\" && value.trim() !== \"\" && Number.isFinite(Number(value))\n\t\t\t\t\t? Number(value)\n\t\t\t\t\t: kind === \"boolean\" && (value === \"true\" || value === \"false\")\n\t\t\t\t\t\t? value === \"true\"\n\t\t\t\t\t\t: value;\n\t}\n\treturn Object.keys(gathered).length === 0 ? raw : { ...rest, [spec.name]: gathered };\n}\n";
196
+ export declare const WIRE_ITSELF_SOURCE = "\n/** What a parameter was sent as itself, out of the object its gatherer made: refused where it was not. */\nfunction wireItself(raw: unknown, ctx: { addIssue(issue: { code: \"custom\"; message: string }): void }): unknown {\n\tif (raw === null || typeof raw !== \"object\") return raw;\n\tconst sent = wireSentAsItself.get(raw);\n\tif (sent === undefined) return raw;\n\tif (sent.length === 0) {\n\t\tctx.addIssue({ code: \"custom\", message: \"Sent as an object's keys, not as a value of its own\" });\n\t\treturn z.NEVER;\n\t}\n\treturn sent[0];\n}\n";
@@ -129,7 +129,153 @@ export function needsExpansionDecoding(segment, shape) {
129
129
  * {@link URI_EXPANSION_SOURCE}, declared once in `schemas.gen.ts` when something calls it.
130
130
  */
131
131
  export function uriExpansionCall(spec) {
132
- return `(raw, ctx) => uriExpansion(raw, ctx, { prefix: ${literal(spec.prefix)}, suffix: ${literal(spec.suffix)}, operator: ${literal(spec.operator)}, name: ${literal(spec.name)}, explode: ${spec.explode}, shape: ${literal(spec.shape)}, values: ${literal(spec.values)} })`;
132
+ return `(raw, ctx) => uriExpansion(raw, ctx, { prefix: ${literal(spec.prefix)}, suffix: ${literal(spec.suffix)}, operator: ${literal(spec.operator)}, name: ${literal(spec.name)}, explode: ${spec.explode}, shape: ${literal(spec.shape)}, ${readersOf(spec.readers)} })`;
133
+ }
134
+ /**
135
+ * A reader for a list: a lone value boxed into the list it stands for, each element read against the
136
+ * element's schema - `?tags=1&tags=2` gathered as `["1", "2"]`, and `?tags=1` as `["1"]`.
137
+ */
138
+ export function eachReader(element, one, whole) {
139
+ const each = typeof element === "string"
140
+ ? element
141
+ : element.length === 1
142
+ ? element[0]
143
+ : `[${element.join(", ")}]`;
144
+ return `{ each: ${each}${one === undefined ? "" : `, one: ${one}`}${whole === undefined ? "" : `, whole: ${whole}`} }`;
145
+ }
146
+ export const NO_READERS = { values: null, fields: null };
147
+ function readersOf(readers) {
148
+ const by = readers.by;
149
+ return by === undefined
150
+ ? tableOf(readers)
151
+ : `${tableOf(readers)}, by: { property: ${literal(by.property)}, readers: { ${Object.entries(by.readers)
152
+ .map(([value, each]) => `[${literal(value)}]: { ${tableOf(each)} }`)
153
+ .join(", ")} } }`;
154
+ }
155
+ /** One table of readers: every value's, and each key's. */
156
+ function tableOf(readers) {
157
+ const fields = readers.fields === null
158
+ ? "null"
159
+ : // Computed keys, so `__proto__` is a key like any other and not the object's prototype.
160
+ `{ ${Object.entries(readers.fields)
161
+ .map(([key, schema]) => `[${literal(key)}]: ${schema}`)
162
+ .join(", ")} }`;
163
+ return `values: ${readers.values ?? "null"}, fields: ${fields}`;
164
+ }
165
+ /**
166
+ * **Wire text read as the JSON value it spells, as its schema says it is.** A path, query, header,
167
+ * cookie or form value is always text, and the document's schema describes it once read: `"5"` is
168
+ * `5` where the schema is `int32`, `true` where it is `boolean`, and stays `"5"` where it is a
169
+ * string. The text is tried as a number, then as a boolean, then as itself, and the first the schema
170
+ * accepts is the value - so `limit: int32 | "all"` reads `"5"` as `5` and `"all"` as `"all"`, and
171
+ * `int32 | boolean` reads both. Where none is accepted the first reading is handed on, to fail with
172
+ * the error that reading earns.
173
+ *
174
+ * **A number is text in JSON's number grammar, and nothing looser.** Read by `Number()`, `"0x10"`,
175
+ * `"007"` and `" 5"` were numbers: `int32` accepted text no JSON number is written as, and `"007" |
176
+ * int32` turned the `"007"` it accepts into `7`.
177
+ *
178
+ * Read by a kind instead - number, boolean or text, decided from the type - a numeric enum, a union
179
+ * of a number and a boolean, and a number beside a string literal were never read at all.
180
+ */
181
+ export const WIRE_READ_SOURCE = `
182
+ /** Wire text as the JSON value it spells that the schema accepts: a number, a boolean, or the text. */
183
+ function wireRead(raw: unknown, schema: z.ZodType): unknown {
184
+ if (typeof raw !== "string") return raw;
185
+ const readings: unknown[] = [];
186
+ if (/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/.test(raw) && Number.isFinite(Number(raw))) {
187
+ readings.push(Number(raw));
188
+ }
189
+ if (raw === "true" || raw === "false") readings.push(raw === "true");
190
+ for (const reading of [...readings, raw]) if (schema.safeParse(reading).success) return reading;
191
+ return readings[0] ?? raw;
192
+ }
193
+ `;
194
+ /**
195
+ * How one value is read: against a schema, or as a list boxed and read element by element - a
196
+ * record's or a model's value, through the decoders below, and a multipart text part that may be one
197
+ * value or several. Declared beside its callers only.
198
+ */
199
+ export const WIRE_READ_AS_SOURCE = `
200
+ /** How one value is read: against a schema, or as a list of values each read against one. */
201
+ type WireReader =
202
+ | z.ZodType
203
+ | { readonly each: z.ZodType | readonly z.ZodType[]; readonly one?: z.ZodType; readonly whole?: z.ZodType };
204
+
205
+ /** A value read against its reader: one value, or a list of them. */
206
+ function wireReadAs(value: unknown, reader: WireReader | null | undefined): unknown {
207
+ if (reader === null || reader === undefined) return value;
208
+ if ("each" in reader) {
209
+ const accepted = (read: unknown): boolean => reader.whole === undefined || reader.whole.safeParse(read).success;
210
+ // A value that may be one value or a list of them: a lone occurrence is the one value, where that is one.
211
+ if (!Array.isArray(value) && reader.one !== undefined) {
212
+ const one = wireRead(value, reader.one);
213
+ if (accepted(one)) return one;
214
+ }
215
+ // A list read as the first of its list members' elements the whole accepts: \`int32[] | string[]\`.
216
+ const items: unknown[] = Array.isArray(value) ? value : [value];
217
+ const lists: readonly z.ZodType[] = Array.isArray(reader.each) ? reader.each : [reader.each as z.ZodType];
218
+ const readings = lists.map((each) => items.map((item) => wireRead(item, each)));
219
+ return readings.find(accepted) ?? readings[0];
220
+ }
221
+ return wireRead(value, reader);
222
+ }
223
+ `;
224
+ /**
225
+ * How the RFC 6570 decoders read a record's or a model's values: by the reader for the key, looked
226
+ * up as the table's OWN property - a key named `constructor` found `Object` on the prototype and
227
+ * handed it to `wireRead`, which threw - each read by {@link WIRE_READ_AS_SOURCE}. Declared beside
228
+ * those decoders only, which are its only callers.
229
+ */
230
+ export const WIRE_READERS_SOURCE = `
231
+ /** A record's or a model's readers: every value's, each key's, and a discriminated base's per subtype. */
232
+ type WireReaders = {
233
+ readonly values: WireReader | null;
234
+ readonly fields: globalThis.Readonly<globalThis.Record<string, WireReader>> | null;
235
+ readonly by?: { readonly property: string; readonly readers: globalThis.Readonly<globalThis.Record<string, WireReaders>> };
236
+ };
237
+
238
+ /** The readers for an object whose discriminator reads \`kind\`: the subtype it names, else the base's. */
239
+ function wireReadersOf(spec: WireReaders, kind: unknown): WireReaders {
240
+ if (spec.by === undefined || typeof kind !== "string") return spec;
241
+ return Object.prototype.hasOwnProperty.call(spec.by.readers, kind) ? (spec.by.readers[kind] ?? spec) : spec;
242
+ }
243
+
244
+ /** The value an object's discriminator holds, as its own property. */
245
+ function wireKindOf(spec: WireReaders, raw: object): unknown {
246
+ return spec.by !== undefined && Object.prototype.hasOwnProperty.call(raw, spec.by.property)
247
+ ? (raw as globalThis.Record<string, unknown>)[spec.by.property]
248
+ : undefined;
249
+ }
250
+
251
+ /** A model's reader for a key it declares, read as its own property only, or a record's for any. */
252
+ function wireReaderOf(spec: WireReaders, key: string): WireReader | null {
253
+ const own = spec.fields !== null && Object.prototype.hasOwnProperty.call(spec.fields, key);
254
+ return (own ? spec.fields?.[key] : spec.values) ?? null;
255
+ }
256
+ `;
257
+ /**
258
+ * **A gathered object read as one member of a union of models reads it.** A form-exploded query
259
+ * typed `A | B` arrives as one set of keys, gathered once under the parameter's name; each member then
260
+ * reads the values as its own properties say. Gathered once per member, the last member's gatherer
261
+ * claimed the name and every other member's keys stayed unread, so `?kind=a&x=1` was refused.
262
+ * Copied key by key as own properties, so a key named `__proto__` is a key and not the prototype.
263
+ */
264
+ export const WIRE_READ_OBJECT_SOURCE = `
265
+ /** A gathered object's values read against one member's readers. */
266
+ function wireReadObject(raw: unknown, spec: WireReaders): unknown {
267
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
268
+ const readers = wireReadersOf(spec, wireKindOf(spec, raw));
269
+ const read: globalThis.Record<string, unknown> = {};
270
+ for (const [key, value] of Object.entries(raw)) {
271
+ Object.defineProperty(read, key, { value: wireReadAs(value, wireReaderOf(readers, key)), enumerable: true, writable: true, configurable: true });
272
+ }
273
+ return read;
274
+ }
275
+ `;
276
+ /** The call that reads a gathered object as one member of a union - see {@link WIRE_READ_OBJECT_SOURCE}. */
277
+ export function wireReadObjectCall(readers) {
278
+ return `(raw) => wireReadObject(raw, { ${readersOf(readers)} })`;
133
279
  }
134
280
  /**
135
281
  * **RFC 6570 section 3.2, run backwards.** A router hands over the text of the segment an expression
@@ -142,20 +288,27 @@ export function uriExpansionCall(spec) {
142
288
  * the refusal to the schema admitted a request the route does not match. A list's element values
143
289
  * are decoded by the element decoder inside, as a query list's are; a record's values are decoded
144
290
  * here, because no schema expression reaches them one by one.
291
+ *
292
+ * **A value of any type is read as the shape its expansion spells** (`any`): `unknown`, a scalar
293
+ * with no standard base, or a union holding one, which openapi3 publishes as `{}` or with `{}` among
294
+ * its members. Under `{;v*}` a scalar is exactly one `v=` part, so `;v=1;v=2` - `[1, 2]` in the
295
+ * document - and `;a=1;b=2` - `{ a: 1, b: 2 }` - had no reading, and were refused as not the
296
+ * expansion the route declares. Several parts all named `v` are a list, any other an object, each
297
+ * value read as the text it spells; anywhere else, and as one `v=` part, every expansion is also a
298
+ * scalar's, which is the reading taken first, as `wireRead` takes the number first.
145
299
  */
146
300
  export const URI_EXPANSION_SOURCE = `
147
301
  /** RFC 6570 section 3.2 in reverse: one expression's expansion, decoded into the value it expanded. */
148
302
  function uriExpansion(
149
303
  raw: unknown,
150
304
  ctx: { addIssue(issue: { code: "custom"; message: string }): void },
151
- spec: {
305
+ spec: WireReaders & {
152
306
  readonly prefix: string;
153
307
  readonly suffix: string;
154
308
  readonly operator: string;
155
309
  readonly name: string;
156
310
  readonly explode: boolean;
157
- readonly shape: "scalar" | "list" | "record";
158
- readonly values: "number" | "boolean" | "";
311
+ readonly shape: "scalar" | "list" | "record" | "any";
159
312
  },
160
313
  ): unknown {
161
314
  if (typeof raw !== "string") return raw;
@@ -170,39 +323,49 @@ function uriExpansion(
170
323
  if (!text.startsWith(spec.operator)) return refuse();
171
324
  text = text.slice(1);
172
325
  }
173
- if (spec.operator === ";" && !(spec.shape === "record" && spec.explode)) {
174
- const named = text
175
- .split(";")
176
- .map((part) => (part === spec.name ? "" : part.startsWith(\`\${spec.name}=\`) ? part.slice(spec.name.length + 1) : undefined));
326
+ const parts = text.split(";");
327
+ const own = parts.filter((part) => part === spec.name || part.startsWith(\`\${spec.name}=\`)).length;
328
+ // A value of any type is the shape its expansion spells: only \`{;v*}\` spells a list or an object no scalar does.
329
+ const shape =
330
+ spec.shape !== "any"
331
+ ? spec.shape
332
+ : spec.operator !== ";" || !spec.explode || text === "" || (parts.length === 1 && own === 1)
333
+ ? "scalar"
334
+ : own === parts.length
335
+ ? "list"
336
+ : "record";
337
+ if (spec.operator === ";" && !(shape === "record" && spec.explode)) {
338
+ const named = parts.map((part) => (part === spec.name ? "" : part.startsWith(\`\${spec.name}=\`) ? part.slice(spec.name.length + 1) : undefined));
177
339
  if (named.some((part) => part === undefined)) return refuse();
178
- if (spec.shape === "list" && spec.explode) return named;
340
+ if (shape === "list" && spec.explode) {
341
+ // Read here where no element decoder runs after: the elements of a value of any type.
342
+ return spec.shape === "any" ? named.map((item) => wireReadAs(item, spec.values)) : named;
343
+ }
179
344
  if (named.length !== 1) return refuse();
180
345
  text = named[0] ?? "";
181
346
  }
182
- if (spec.shape === "scalar") return text;
347
+ if (shape === "scalar") return text;
183
348
  const joiner = spec.explode && (spec.operator === "." || spec.operator === "/" || spec.operator === ";") ? spec.operator : ",";
184
- if (spec.shape === "list") return text === "" ? [] : text.split(joiner);
185
- const value = (item: string): unknown =>
186
- spec.values === "number" && item.trim() !== "" && Number.isFinite(Number(item))
187
- ? Number(item)
188
- : spec.values === "boolean" && (item === "true" || item === "false")
189
- ? item === "true"
190
- : item;
191
- const entries: [string, unknown][] = [];
349
+ if (shape === "list") return text === "" ? [] : text.split(joiner);
350
+ const entries: [string, string][] = [];
192
351
  if (spec.explode) {
193
- for (const pair of text === "" ? [] : text.split(joiner)) {
194
- const at = pair.indexOf("=");
195
- if (at === -1) return refuse();
196
- entries.push([pair.slice(0, at), value(pair.slice(at + 1))]);
352
+ for (const piece of text === "" ? [] : text.split(joiner)) {
353
+ const at = piece.indexOf("=");
354
+ const last = entries[entries.length - 1];
355
+ // A value holding the joiner, which the expansion leaves as it is - \`a=1.5\` under \`{.r*}\` - goes on.
356
+ if (at === -1 && last !== undefined) last[1] += \`\${joiner}\${piece}\`;
357
+ else if (at === -1) return refuse();
358
+ else entries.push([piece.slice(0, at), piece.slice(at + 1)]);
197
359
  }
198
360
  } else {
199
361
  const items = text === "" ? [] : text.split(",");
200
362
  if (items.length % 2 !== 0) return refuse();
201
363
  for (let index = 0; index < items.length; index += 2) {
202
- entries.push([items[index] ?? "", value(items[index + 1] ?? "")]);
364
+ entries.push([items[index] ?? "", items[index + 1] ?? ""]);
203
365
  }
204
366
  }
205
- return Object.fromEntries(entries);
367
+ const readers = wireReadersOf(spec, wireKindOf(spec, Object.fromEntries(entries)));
368
+ return Object.fromEntries(entries.map(([key, item]) => [key, wireReadAs(item, wireReaderOf(readers, key))]));
206
369
  }
207
370
  `;
208
371
  /**
@@ -210,49 +373,91 @@ function uriExpansion(
210
373
  * gatherer itself is {@link EXPLODED_QUERY_SOURCE}.
211
374
  */
212
375
  export function explodedQueryCall(spec) {
213
- return `(raw) => explodedQuery(raw, { name: ${literal(spec.name)}, others: ${literal(spec.others)}, keys: ${literal(spec.keys ?? null)}, numbers: ${literal(spec.numbers)}, booleans: ${literal(spec.booleans)}, values: ${literal(spec.values)} })`;
376
+ return `(raw) => explodedQuery(raw, { name: ${literal(spec.name)}, others: ${literal(spec.others)}, keys: ${literal(spec.keys ?? null)}, itself: ${spec.itself}, ${readersOf(spec.readers)} })`;
214
377
  }
215
378
  /**
216
379
  * **`{?param*}` over a record or a model expands to the object's own keys**, `?a=1&b=2` and
217
380
  * `?field=status&value=active`, so there is no `param` key for a query validator to find. This
218
381
  * gathers those keys back under the parameter's name before the query object is validated: a model
219
382
  * takes exactly its declared properties, a record every key no other query parameter claims.
383
+ *
384
+ * **The parameter's own name is a key like any other, and only that.** `?f=5` beside `f?: F`, where
385
+ * `F` declares no `f`, is no key of the object and no other parameter's: the document reads it as a
386
+ * parameter it does not declare, and accepts it. Left under the name, it reached the model's schema
387
+ * as the string `"5"`, and was refused, where `?zzz=5` was accepted. Only a parameter that may also be
388
+ * sent as itself keeps it - see {@link WIRE_ITSELF_SOURCE}.
220
389
  */
221
390
  export const EXPLODED_QUERY_SOURCE = `
391
+ /** Each object a gatherer made for a parameter that may also be sent as itself, and what was: one value, or none. */
392
+ const wireSentAsItself = new WeakMap<object, readonly unknown[]>();
393
+
222
394
  /** RFC 6570 form expansion of an exploded record or model, gathered back under its parameter. */
223
395
  function explodedQuery(
224
396
  raw: unknown,
225
- spec: {
397
+ spec: WireReaders & {
226
398
  readonly name: string;
227
399
  readonly others: readonly string[];
228
400
  readonly keys: readonly string[] | null;
229
- readonly numbers: readonly string[];
230
- readonly booleans: readonly string[];
231
- readonly values: "number" | "boolean" | "";
401
+ readonly itself: boolean;
232
402
  },
233
403
  ): unknown {
234
404
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
235
- const gathered: Record<string, unknown> = {};
236
- const rest: Record<string, unknown> = {};
405
+ const readers = wireReadersOf(spec, wireKindOf(spec, raw));
406
+ const gathered: globalThis.Record<string, unknown> = {};
407
+ const rest: globalThis.Record<string, unknown> = {};
237
408
  for (const [key, value] of Object.entries(raw)) {
238
409
  // Already gathered only where an OBJECT sits under the name: a query value from the wire is a
239
410
  // string or a list of them, and \`{ param: 0 }\` expands to \`?param=0\`.
240
411
  if (key === spec.name && value !== null && typeof value === "object" && !Array.isArray(value)) return raw;
241
- const own = spec.keys === null ? !spec.others.includes(key) : spec.keys.includes(key);
242
- if (!own) {
243
- rest[key] = value;
244
- continue;
245
- }
246
- const kind = spec.values !== "" ? spec.values : spec.numbers.includes(key) ? "number" : spec.booleans.includes(key) ? "boolean" : "";
247
- gathered[key] =
248
- typeof value !== "string"
249
- ? value
250
- : kind === "number" && value.trim() !== "" && Number.isFinite(Number(value))
251
- ? Number(value)
252
- : kind === "boolean" && (value === "true" || value === "false")
253
- ? value === "true"
254
- : value;
412
+ /**
413
+ * Left in place as well, for every other object gathered from the same query: \`f: F1\` and
414
+ * \`g: F2\` both declaring \`a\` each read \`?a=1\`, as the document reads it, where the first
415
+ * gatherer took \`a\` and the second refused a missing \`g\`. The query object strips it after.
416
+ */
417
+ rest[key] = value;
418
+ // A key another query parameter is named is that parameter's, the reading the document takes.
419
+ const own = !spec.others.includes(key) && (spec.keys === null || spec.keys.includes(key));
420
+ if (own) gathered[key] = wireReadAs(value, wireReaderOf(readers, key));
421
+ }
422
+ const sent = Object.prototype.hasOwnProperty.call(raw, spec.name);
423
+ if (Object.keys(gathered).length > 0) {
424
+ if (spec.itself) wireSentAsItself.set(gathered, sent ? [(raw as globalThis.Record<string, unknown>)[spec.name]] : []);
425
+ return { ...rest, [spec.name]: gathered };
426
+ }
427
+ // Nothing gathered: the name sent alone is the parameter itself, where it may be, or a key nothing declares.
428
+ if (!sent || spec.itself) return raw;
429
+ delete rest[spec.name];
430
+ return rest;
431
+ }
432
+ `;
433
+ /** The call that hands a list or a scalar form the value its parameter was sent as itself. */
434
+ export function wireItselfCall() {
435
+ return `(raw, ctx) => wireItself(raw, ctx)`;
436
+ }
437
+ /**
438
+ * **A parameter that may be an object or a list is read both ways from one query.** `x?: int32[] |
439
+ * Record<int32>` exploded is `?x=1&x=2` as the list and `?a=1&b=2` as the record - and `?x=5` as
440
+ * either, since `{ x: 5 }` expands to it too. The record's gatherer takes every key, the parameter's
441
+ * own name included, so the list member was handed `{ x: ["1", "2"] }` and refused it: `?x=1&x=2`,
442
+ * `[1, 2]` in the document, was refused for every such union, and `?x=007`, `["007"]`, for
443
+ * `string[] | Record<int32>`. The gatherer now records what arrived under the name beside the object
444
+ * it made, and each member that is not an object reads that, while each that is reads the object -
445
+ * so the union's first member the value satisfies is its reading, as everywhere else.
446
+ *
447
+ * **Only an object the gatherer made carries it.** One a caller built - the value already gathered,
448
+ * or `queryFields` given structured input - is the object it is, and a member that is not an object
449
+ * refuses it as before; the gatherer's own object, with nothing sent under the name, is refused here.
450
+ */
451
+ export const WIRE_ITSELF_SOURCE = `
452
+ /** What a parameter was sent as itself, out of the object its gatherer made: refused where it was not. */
453
+ function wireItself(raw: unknown, ctx: { addIssue(issue: { code: "custom"; message: string }): void }): unknown {
454
+ if (raw === null || typeof raw !== "object") return raw;
455
+ const sent = wireSentAsItself.get(raw);
456
+ if (sent === undefined) return raw;
457
+ if (sent.length === 0) {
458
+ ctx.addIssue({ code: "custom", message: "Sent as an object's keys, not as a value of its own" });
459
+ return z.NEVER;
255
460
  }
256
- return Object.keys(gathered).length === 0 ? raw : { ...rest, [spec.name]: gathered };
461
+ return sent[0];
257
462
  }
258
463
  `;