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 CHANGED
@@ -1,14 +1,60 @@
1
1
  # typespec-http-zod
2
2
 
3
- Generate Zod validators and framework-free TypeScript types from a TypeSpec HTTP service.
3
+ Zod validators generated from the same API definition as your OpenAPI document.
4
4
 
5
- The emitted validators agree, keyword for keyword, with the OpenAPI document
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
- It is also a library. [`typespec-hono`](https://github.com/bison-digital/typespec-hono) is an emitter
11
- built on its API.
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[]>;