typespec-http-zod 0.32.0 → 0.33.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/src/api.d.ts CHANGED
@@ -575,6 +575,43 @@ export interface EmittedService {
575
575
  readonly routes: readonly EmittedRoute[];
576
576
  /** The identifiers every per-operation validator was declared under, keyed by operation id. */
577
577
  readonly schemaNames: ReadonlyMap<string, RouteSchemaNames>;
578
+ /**
579
+ * **The identifier each declared type's validator is exported under in `schemas.gen.ts`**, keyed
580
+ * by the type: a model, union or enum the document publishes as a component of its own. A scalar
581
+ * is validated inline wherever it is used and has no entry.
582
+ *
583
+ * **Keyed by the types this service was emitted from**, which are the ones `operations` holds: a
584
+ * walk from an operation's response bodies, through each base's `derivedModels`, reaches the keys
585
+ * themselves. For a versioned service those are the types projected to its latest version, not
586
+ * the types as declared, which hold every version at once.
587
+ *
588
+ * **A body is keyed by the model it is, its effective payload type.** Where visibility reshapes a
589
+ * model, `@typespec/http` hands its body over as a copy - `Widget` returned beside a create-only
590
+ * property is `{ ...Widget }` - which is not a key and has no `derivedModels`. openapi3 resolves it
591
+ * to `Widget` with `getEffectivePayloadType`, and so does a lookup:
592
+ * `createMetadataInfo(program, { canonicalVisibility: Visibility.Read })
593
+ * .getEffectivePayloadType(body.type, Visibility.Read)`.
594
+ *
595
+ * **The declaration under the type's own component name**, which is the one a response reads
596
+ * wherever a response reads the type: `Widget` -> `widgetSchema`, beside a create view
597
+ * `widgetCreateSchema` that is not in this map. `@friendlyName` names it and a reserved word takes
598
+ * the module's `_`, as the module declares it: `@friendlyName("Canine") model Hound` ->
599
+ * `canineSchema`, `` model `default` `` -> `default_Schema`. A multipart body's model is here,
600
+ * validated as the form the document publishes. A type the document publishes only under views of
601
+ * it, sent in create and update bodies and never returned, has none.
602
+ *
603
+ * **Every subtype of a discriminated base is here**, nested bases included and whether or not an
604
+ * operation names it, so a wrapping emitter can tell which subtype a value is by the subtype's
605
+ * own validator: the base's parsed output does not say which arm of its dispatch matched.
606
+ *
607
+ * **A declaration that validates nothing has no entry** - `duplicate-declaration`,
608
+ * `split-visibility` and every other refusal of a whole type - so a lookup answers `undefined`
609
+ * rather than a validator that refuses every value.
610
+ *
611
+ * Published so that an emitter building on this one imports a declared type's validator by name
612
+ * rather than agreeing with this package about how the name is spelled.
613
+ */
614
+ readonly typeSchemaNames: ReadonlyMap<Type, string>;
578
615
  /**
579
616
  * **The operation each route was collected from, keyed by operation id**, so a wrapping emitter
580
617
  * reading its doc comment, tags or diagnostic target reads the right one. Verb and path do not
package/dist/src/api.js CHANGED
@@ -4184,6 +4184,7 @@ export async function emitHttpZod(context) {
4184
4184
  service,
4185
4185
  routes,
4186
4186
  schemaNames: names,
4187
+ typeSchemaNames: registry.typeIdentifiers(),
4187
4188
  operations,
4188
4189
  outputDir,
4189
4190
  options: resolved,
@@ -119,6 +119,16 @@ export declare class SchemaRegistry {
119
119
  declareNamed(name: string, build: () => string, site: Type): string;
120
120
  /** Every declaration, dependencies first. */
121
121
  declarations(): readonly Declaration[];
122
+ /**
123
+ * **The identifier each type is declared under by its own component name** - see
124
+ * `EmittedService.typeSchemaNames`. That is the declaration a response reads, wherever one does:
125
+ * `Read` is never suffixed, so a view of the type (`WidgetCreate`) and a 3.0 request's reading of
126
+ * it are other declarations, and neither is the one a wrapping emitter resolves a value by. A
127
+ * multipart body is its model's, which the document publishes under the model's name as the form it
128
+ * is; an envelope is named for its union and variant, and is no type's own. A declaration that
129
+ * validates nothing is left out rather than published as a validator.
130
+ */
131
+ typeIdentifiers(): ReadonlyMap<Type, string>;
122
132
  /** The names of the declarations that validate nothing, refused here or in the walk beneath. */
123
133
  refusedNames(): ReadonlySet<string>;
124
134
  }
@@ -495,6 +495,8 @@ export class SchemaRegistry {
495
495
  #withheld = new Set();
496
496
  /** Every type name a refused identifier stood for: `Foo` and `foo` are both `fooSchema`. */
497
497
  #refusedTypeNames = new Set();
498
+ /** Each type declared by another type's declaration, the two rendered alike under one name. */
499
+ #alike = [];
498
500
  /**
499
501
  * Inlined types currently being walked, by identity, against the number of declarations that were
500
502
  * in flight when each was entered.
@@ -632,6 +634,9 @@ export class SchemaRegistry {
632
634
  */
633
635
  if (twin !== undefined && twin.source === declaration.source) {
634
636
  this.#declarations.set(key, twin);
637
+ // Another type of the name, rendered alike, is validated by it too - see `typeIdentifiers`.
638
+ if (twin.type !== type)
639
+ this.#alike.push({ type, identifier });
635
640
  return twin.identifier;
636
641
  }
637
642
  if (twin !== undefined) {
@@ -1057,6 +1062,50 @@ export class SchemaRegistry {
1057
1062
  declarations() {
1058
1063
  return this.#order;
1059
1064
  }
1065
+ /**
1066
+ * **The identifier each type is declared under by its own component name** - see
1067
+ * `EmittedService.typeSchemaNames`. That is the declaration a response reads, wherever one does:
1068
+ * `Read` is never suffixed, so a view of the type (`WidgetCreate`) and a 3.0 request's reading of
1069
+ * it are other declarations, and neither is the one a wrapping emitter resolves a value by. A
1070
+ * multipart body is its model's, which the document publishes under the model's name as the form it
1071
+ * is; an envelope is named for its union and variant, and is no type's own. A declaration that
1072
+ * validates nothing is left out rather than published as a validator.
1073
+ */
1074
+ typeIdentifiers() {
1075
+ const found = new Map();
1076
+ const consider = (type, declaration) => {
1077
+ if (this.#validatesNothing(declaration))
1078
+ return;
1079
+ // A view, and a 3.0 request's reading, are named with a suffix - see `nameAt`.
1080
+ const bare = declaredNameOf(this.#program, type);
1081
+ if (bare === undefined || declaration.typeName !== safeDeclarationName(bare))
1082
+ return;
1083
+ found.set(type, declaration.identifier);
1084
+ };
1085
+ for (const declaration of this.#order) {
1086
+ const { type, synthetic } = declaration;
1087
+ if (type !== undefined)
1088
+ consider(type, declaration);
1089
+ else if (synthetic?.kind === "multipart")
1090
+ consider(synthetic.site, declaration);
1091
+ }
1092
+ // By identifier, because the declaration a type shares can be rendered again since.
1093
+ for (const { type, identifier } of this.#alike) {
1094
+ const declaration = this.#order.find((each) => each.identifier === identifier);
1095
+ if (declaration !== undefined)
1096
+ consider(type, declaration);
1097
+ }
1098
+ return found;
1099
+ }
1100
+ /**
1101
+ * **Rendered as `z.never()`, and described where it has a doc comment**, so its source is not always
1102
+ * `UNREPRESENTABLE` itself: a model split by visibility keeps its description. A declaration refused
1103
+ * as two shapes under one name is replaced by a bare `UNREPRESENTABLE`, so it is one of these too.
1104
+ */
1105
+ #validatesNothing(declaration) {
1106
+ return (declaration.source === UNREPRESENTABLE ||
1107
+ declaration.source.startsWith(`${UNREPRESENTABLE}.describe(`));
1108
+ }
1060
1109
  /** The names of the declarations that validate nothing, refused here or in the walk beneath. */
1061
1110
  refusedNames() {
1062
1111
  return new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
5
5
  "keywords": [
6
6
  "codegen",