typespec-http-zod 0.23.0 → 0.25.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/README.md +61 -7
- package/dist/src/api.d.ts +34 -12
- package/dist/src/api.js +315 -114
- package/dist/src/constraints.d.ts +7 -6
- package/dist/src/constraints.js +7 -6
- package/dist/src/lib.d.ts +43 -1
- package/dist/src/lib.js +46 -0
- package/dist/src/registry.d.ts +41 -0
- package/dist/src/registry.js +80 -19
- package/dist/src/types.js +29 -8
- package/dist/src/zod.d.ts +27 -0
- package/dist/src/zod.js +64 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,14 +1,60 @@
|
|
|
1
1
|
# typespec-http-zod
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zod validators generated from the same API definition as your OpenAPI document.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[`@typespec/openapi3`](https://typespec.io) publishes from the same source. Use them with Express,
|
|
7
|
-
Fastify, Elysia, a Workers fetch handler, a typed client, or a test suite. No server framework is
|
|
8
|
-
required.
|
|
5
|
+
## What is it?
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
An emitter for [TypeSpec](https://typespec.io), Microsoft's language for describing APIs. Describe
|
|
8
|
+
your API once, and this package generates the Zod schemas that validate every request and response,
|
|
9
|
+
plus the TypeScript types to go with them.
|
|
10
|
+
|
|
11
|
+
TypeSpec compiles to OpenAPI as well, so the same definition also gives you documentation that cannot
|
|
12
|
+
drift from the validation.
|
|
13
|
+
|
|
14
|
+
One model:
|
|
15
|
+
|
|
16
|
+
```tsp
|
|
17
|
+
model Widget {
|
|
18
|
+
id: string;
|
|
19
|
+
|
|
20
|
+
@minLength(1)
|
|
21
|
+
name: string;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The validators and types this package generates:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export const widgetSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
name: z.string().min(1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type Widget = z.infer<typeof widgetSchema>;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
And the OpenAPI that [`@typespec/openapi3`](https://typespec.io) generates from that same model:
|
|
37
|
+
|
|
38
|
+
```jsonc
|
|
39
|
+
"Widget": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["id", "name"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"id": { "type": "string" },
|
|
44
|
+
"name": { "type": "string", "minLength": 1 }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- **Documentation and validation cannot drift** - both come from one definition, so there is no
|
|
52
|
+
second source to keep in step
|
|
53
|
+
- **One runtime dependency** - `zod`, and nothing else
|
|
54
|
+
- **No framework** - use the validators in a server, a client, or a test
|
|
55
|
+
- **Types with zero imports** - a contracts module anything can depend on
|
|
56
|
+
- **Built on a public API** - [`typespec-hono`](https://github.com/bison-digital/typespec-hono)
|
|
57
|
+
generates a whole Hono server from it
|
|
12
58
|
|
|
13
59
|
## Install
|
|
14
60
|
|
|
@@ -138,6 +184,12 @@ then a range such as `4XX`, then `default`.
|
|
|
138
184
|
| `vocabularies.gen.ts` | each enum's members as a runtime tuple | `contracts-output-dir` is set |
|
|
139
185
|
| `wire-contract.gen.ts` | assertions pairing the emitted Zod against those types | that and `contracts-package` |
|
|
140
186
|
|
|
187
|
+
`wire-contract.gen.ts` is not imported by any other file. The file contains type-level assertions
|
|
188
|
+
pairing each generated validator with its contract type, so a mismatch between the two is a compile
|
|
189
|
+
error. TypeScript reaches the file through `tsconfig`'s `include`, which has two consequences worth
|
|
190
|
+
knowing before removing it: dependency scanners report the file as unused, and deleting it does not
|
|
191
|
+
break the build until the validator and the contract type diverge.
|
|
192
|
+
|
|
141
193
|
Identifiers are named from the operation id the document publishes. An operation inside a namespace
|
|
142
194
|
carries the whole id, so `op readWidget` in `namespace Widgets` becomes `Widgets_readWidgetPath`.
|
|
143
195
|
|
|
@@ -148,6 +200,8 @@ carries the whole id, so `op readWidget` in `namespace Widgets` becomes `Widgets
|
|
|
148
200
|
- [Reference](docs/reference.md): every option, every diagnostic, and the known limits.
|
|
149
201
|
- [Oracles](docs/oracles.md): every artefact this emitter produces, and what compares it to the
|
|
150
202
|
thing it has to agree with.
|
|
203
|
+
- [Releasing](docs/releasing.md): why this package publishes first, and how to rehearse a
|
|
204
|
+
two-package release against a local registry.
|
|
151
205
|
|
|
152
206
|
## Licence
|
|
153
207
|
|
package/dist/src/api.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export { EmitterOptionsSchema, type EmitterOptions } from "./lib.js";
|
|
|
42
42
|
* emitted module stopped being loadable. This is not a style choice a second emitter may make
|
|
43
43
|
* independently.
|
|
44
44
|
*/
|
|
45
|
-
export { objectKey } from "./zod.js";
|
|
45
|
+
export { jsDocComment, objectKey } from "./zod.js";
|
|
46
46
|
export declare function beginBanner(hint: string | undefined): void;
|
|
47
47
|
/**
|
|
48
48
|
* The status a row registers: the first non-error status the operation declares.
|
|
@@ -69,6 +69,22 @@ export declare function successStatusesOf(operation: HttpOperation): number[];
|
|
|
69
69
|
* listed above", and inventing one would be answering a question the contract left open.
|
|
70
70
|
*/
|
|
71
71
|
export type StatusKey = number | "default" | `${1 | 2 | 3 | 4 | 5}XX`;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the operation declares `@useAuth(NoAuth)` - no caller is established for it.
|
|
74
|
+
*
|
|
75
|
+
* **`@useAuth(NoAuth)` reaches OpenAPI as `security: []`, which is why this is derivable at all.**
|
|
76
|
+
* Whether an operation needs a caller is a contract fact the document publishes. What KIND of caller
|
|
77
|
+
* - account-scoped, resource-scoped - is not: no OpenAPI keyword expresses it, and a server emitter
|
|
78
|
+
* that guesses is enforcing something no document states.
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* One requirement the document publishes: scheme name to the scopes it demands.
|
|
82
|
+
*
|
|
83
|
+
* Satisfying **any one** requirement authorises a caller, and every scheme WITHIN a requirement must
|
|
84
|
+
* be satisfied together -- which is what an array of OpenAPI `security` objects means, and why this
|
|
85
|
+
* is published as a list of lists rather than a flat set of scopes.
|
|
86
|
+
*/
|
|
87
|
+
export type SecurityRequirement = Readonly<Record<string, readonly string[]>>;
|
|
72
88
|
/**
|
|
73
89
|
* The body every `@error` response in the service declares, when they all declare the same one.
|
|
74
90
|
*
|
|
@@ -298,6 +314,21 @@ export interface EmittedRoute {
|
|
|
298
314
|
* `requireScopes(...)` repeated per route that nothing checks against it.
|
|
299
315
|
*/
|
|
300
316
|
readonly scopes: readonly string[];
|
|
317
|
+
/**
|
|
318
|
+
* What the DOCUMENT says a caller must satisfy, in the shape the document says it.
|
|
319
|
+
*
|
|
320
|
+
* **`scopes` and `noAuth` are lossy projections of this, and publishing only those was a defect
|
|
321
|
+
* both server emitters had to work around.** A flat scope union cannot say WHICH scheme demanded
|
|
322
|
+
* what, so `@useAuth(BearerAuth)` -- which reaches OpenAPI as `security: [{ "BearerAuth": [] }]`
|
|
323
|
+
* -- arrived here as an empty scope list indistinguishable from no authentication at all. An
|
|
324
|
+
* emitter wanting the gate the document publishes had to call `getAuthenticationForOperation`
|
|
325
|
+
* again and rebuild this itself, and two of them did.
|
|
326
|
+
*
|
|
327
|
+
* Satisfying **any one** requirement authorises; every scheme within a requirement must be
|
|
328
|
+
* satisfied together. Empty where the operation declares `@useAuth(NoAuth)` or nothing, which is
|
|
329
|
+
* a fact rather than an absence.
|
|
330
|
+
*/
|
|
331
|
+
readonly security: readonly SecurityRequirement[];
|
|
301
332
|
/** Which of two success statuses to answer, and the literal property that decides it. */
|
|
302
333
|
readonly statusBy: {
|
|
303
334
|
property: string;
|
|
@@ -373,6 +404,8 @@ export interface ResolvedServiceOptions {
|
|
|
373
404
|
readonly contractsOutputDir: string | undefined;
|
|
374
405
|
readonly contractsPackage: string | undefined;
|
|
375
406
|
readonly sealObjectSchemas: boolean;
|
|
407
|
+
/** Whether every emitted validator is wrapped in `z.compile()`. See the option's own docblock. */
|
|
408
|
+
readonly compileSchemas: boolean;
|
|
376
409
|
readonly keyVocabularies: readonly string[];
|
|
377
410
|
readonly runtimeModule: string;
|
|
378
411
|
/** The command that regenerates, for the banner. `undefined` keeps the generic line. */
|
|
@@ -403,15 +436,4 @@ export interface EmitHttpZodOptions {
|
|
|
403
436
|
/** The specifier generated files import their runtime contract from when the consumer sets none. */
|
|
404
437
|
readonly defaultRuntimeModule?: string;
|
|
405
438
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Emit this library's artefacts for every `@service` in the program, and hand back what was emitted.
|
|
408
|
-
*
|
|
409
|
-
* **The whole orchestration lives here, once.** The nesting below is load-bearing - a walk that
|
|
410
|
-
* runs outside `collectExternalImports` throws, vocabularies must be collected from what the service
|
|
411
|
-
* DECLARES rather than from what the Zod walk happens to touch, and the error body has to be
|
|
412
|
-
* registered inside the walk or its validator never reaches `schemas.gen.ts`. An emitter built on this
|
|
413
|
-
* library calls this rather than restating it: two copies of a sequence that must agree are two
|
|
414
|
-
* copies that will not, and the symptom would be a subtly different set of declarations in a file
|
|
415
|
-
* whose name is the contract.
|
|
416
|
-
*/
|
|
417
439
|
export declare function emitHttpZod(context: EmitContext, wrapper?: EmitHttpZodOptions): Promise<readonly EmittedService[]>;
|