typespec-http-zod 0.29.0 → 0.31.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.
@@ -1,5 +1,6 @@
1
1
  import { type ModelProperty, type Program, type Type } from "@typespec/compiler";
2
2
  import { Visibility } from "@typespec/http";
3
+ import { type HeldDeclarations, type PropertyPosition } from "./zod.js";
3
4
  /**
4
5
  * Assigns each named TypeSpec model/enum a single Zod declaration, emitted in dependency order.
5
6
  *
@@ -47,6 +48,10 @@ interface Declaration {
47
48
  readonly identifier: string;
48
49
  readonly typeName: string;
49
50
  readonly source: string;
51
+ /** The type declared, to tell one type in two contexts from two types of one name. */
52
+ readonly type?: Type;
53
+ /** The visibility it is declared at, to tell one type at two visibilities from two contexts. */
54
+ readonly at?: Visibility;
50
55
  /**
51
56
  * The structural TypeScript body for a declaration on a cycle, emitted INSTEAD of the
52
57
  * `z.infer<typeof ...>` alias - which cannot be used here without closing the type-level loop.
@@ -54,47 +59,20 @@ interface Declaration {
54
59
  readonly structural?: string;
55
60
  /** Whether the declaration is annotated `: z.ZodType<typeName>`, which `z.lazy()` requires. */
56
61
  readonly annotated?: boolean;
62
+ /** The registry key it was declared under, and its metadata context, to render it again. */
63
+ readonly key?: string;
64
+ readonly ignore?: boolean;
65
+ /** A shape no type stands for, and the type it is published for - see `declareNamed`. */
66
+ readonly synthetic?: Synthetic;
67
+ /** The keys a merge in it asked to be refused, to render it again when one is - see `refusedAt`. */
68
+ readonly consulted?: ReadonlySet<string>;
57
69
  }
58
- /**
59
- * One model can need MORE THAN ONE declaration.
60
- *
61
- * **Keyed on `(type, visibility)`, because a model is not one shape.** `@visibility` and HTTP
62
- * metadata both mean the same declaration projects differently by position: `@typespec/openapi3`
63
- * emits `VisibilityModel` and `VisibilityModelCreate` as separate components for exactly this
64
- * reason. Keyed on the type alone, whichever position was walked first won and every other position
65
- * silently got its shape - measured on the corpus, one strict model carrying all six lifecycle
66
- * properties, so a create request was required to send read-only fields.
67
- *
68
- * The suffix rule is openapi3's: canonical (`Read`) keeps the bare name, and another visibility only
69
- * earns a suffix when the model is actually TRANSFORMED at it. Same rule, so the identifiers line up
70
- * with the component names the document publishes.
71
- */
72
- /**
73
- * The visibility a type is actually DECLARED at, which is not always the one requested.
74
- *
75
- * **A type not reshaped at this visibility IS the canonical declaration, not a copy of it.** Keying
76
- * on the requested visibility while only suffixing transformed types produced two cache entries
77
- * sharing one identifier and an emitted file that stopped parsing:
78
- * `Identifier 'approvalTierSchema' has already been declared`. An enum is the same enum in a request
79
- * and a response, which is what the `Model` guard is for.
80
- *
81
- * It is the same collapse `@typespec/openapi3` performs before naming a component, and
82
- * `isTransformedBy` is its own predicate, `metadataInfo.isTransformed`.
83
- *
84
- * **Extracted so both registries answer it identically.** They must: the emitted assertion pairs a
85
- * validator with a contract type BY NAME, so a rule applied in one walk and paraphrased in the other
86
- * is a pairing that silently compares the wrong two things.
87
- */
88
- export declare function visibilityFor(program: Program, type: Type, requested: Visibility): Visibility;
89
- /**
90
- * A declaration's name at a visibility: the bare name at canonical, suffixed anywhere else.
91
- *
92
- * The suffix is `@typespec/http`'s `getVisibilitySuffix`, called with `Visibility.Read` as the
93
- * canonical exactly as `@typespec/openapi3` calls it in `schema-emitter.ts`. Same function, same
94
- * argument, so these identifiers line up with the component names the document publishes rather than
95
- * merely resembling them.
96
- */
97
- export declare function nameAt(bare: string, at: Visibility): string;
70
+ /** What a declaration no type stands for is: a multipart body, or a `@discriminated` envelope. */
71
+ interface Synthetic {
72
+ readonly kind: "multipart" | "envelope";
73
+ readonly site: Type;
74
+ }
75
+ export declare function nameAt(bare: string, at: Visibility, type: Type): string;
98
76
  export declare class SchemaRegistry {
99
77
  #private;
100
78
  constructor(program: Program);
@@ -103,15 +81,27 @@ export declare class SchemaRegistry {
103
81
  * inline source. Declaring is depth-first so a model's dependencies are emitted above it, which is
104
82
  * what lets the generated file be plain `const` declarations with no forward references.
105
83
  */
106
- expressionFor(type: Type): string;
84
+ expressionFor(given: Type, payload?: boolean): string;
85
+ /** What the contract types' walk asks of these declarations, so a merge is `never` on both sides. */
86
+ get held(): HeldDeclarations;
87
+ /**
88
+ * Declare a component the document publishes, at the visibility the walk is at, as a type
89
+ * another refers to - see `publishedComponents`. Nothing refers to it here, so a declaration
90
+ * still in flight is no back edge.
91
+ */
92
+ declare(type: Type): void;
107
93
  /**
108
94
  * The Zod for a single property, constraints and optionality included.
109
95
  *
110
96
  * Used for path/query parameters, which are `ModelProperty`s that belong to no model - so they
111
97
  * need the property-level walk rather than the type-level one, or `@minLength` on a path param
112
98
  * would be documented and never enforced.
99
+ *
100
+ * **The property is where a refusal inside it points**, as a model's member is - see
101
+ * `withDeclarationSite`. A parameter is walked outside any model, so `@query a?: never[]` was
102
+ * reported at `<unknown location>:1:1`, the intrinsic's own place, which names nothing to edit.
113
103
  */
114
- expressionForProperty(property: ModelProperty): string;
104
+ expressionForProperty(property: ModelProperty, position?: PropertyPosition): string;
115
105
  /**
116
106
  * A declaration for a shape the spec never wrote, under a name the document publishes.
117
107
  *
@@ -124,9 +114,11 @@ export declare class SchemaRegistry {
124
114
  * body: the document publishes `MultiPartRequest` as a component, and there is no walkable type
125
115
  * for it - the parts have to be assembled from `@typespec/http`'s own view of the payload.
126
116
  */
127
- declareNamed(name: string, build: () => string): string;
117
+ declareNamed(name: string, build: () => string, site: Type): string;
128
118
  /** Every declaration, dependencies first. */
129
119
  declarations(): readonly Declaration[];
120
+ /** The names of the declarations that validate nothing, refused here or in the walk beneath. */
121
+ refusedNames(): ReadonlySet<string>;
130
122
  }
131
123
  export interface TsDeclaration {
132
124
  readonly name: string;
@@ -135,11 +127,42 @@ export interface TsDeclaration {
135
127
  /** `true` for an enum-derived alias - declared but not exported, see below. */
136
128
  readonly isVocabulary: boolean;
137
129
  readonly source: string;
130
+ /** The type declared, to tell one type in two contexts from two types of one name. */
131
+ readonly type?: Type;
132
+ /** The visibility it is declared at - see {@link Declaration.at}. */
133
+ readonly at?: Visibility;
134
+ /** A shape no type stands for - see {@link Declaration.synthetic}. */
135
+ readonly synthetic?: Synthetic;
138
136
  }
137
+ /**
138
+ * The same walk as {@link SchemaRegistry}, emitting plain TypeScript instead of Zod.
139
+ *
140
+ * Kept as a separate class rather than a mode on the schema registry because the two produce
141
+ * different *kinds* of declaration - an `interface` versus a `const` - and their reference resolvers
142
+ * hand back different text. Sharing {@link declaredNameOf} is the part that matters: a model named in
143
+ * one artefact is named the same way in the other, so the emitted assertion can pair them by name.
144
+ */
145
+ /**
146
+ * Whether a rendered body is ONE object literal, and so may be emitted as an `interface`.
147
+ *
148
+ * **This was `source.startsWith("{") && !source.includes("} & ")`, which named the two shapes it had
149
+ * met rather than the property it needed.** A discriminated union with an envelope renders as
150
+ * `{ kind: "cat"; value: Cat; } | { kind: "dog"; value: Dog; }`: it starts with a brace and holds no
151
+ * intersection, so it was emitted as `export interface PetWithEnvelope { ... } | { ... }`. An
152
+ * interface cannot be a union, so the file did not parse at all - and that is the DEFAULT envelope,
153
+ * the shape a spec gets from writing `@discriminated` and nothing else.
154
+ *
155
+ * Balanced braces answer the question actually being asked: an interface is valid exactly when the
156
+ * whole body is a single `{...}`, whatever it contains. A composition operator cannot be missed,
157
+ * because anything following the matching close brace fails the test whatever it happens to be.
158
+ */
159
+ export declare function isSingleObjectLiteral(source: string): boolean;
139
160
  export declare class TypeRegistry {
140
161
  #private;
141
- constructor(program: Program);
142
- expressionFor(type: Type): string;
162
+ constructor(program: Program, held?: HeldDeclarations);
163
+ expressionFor(given: Type, payload?: boolean): string;
164
+ /** Declare a component the document publishes, as {@link SchemaRegistry.declare} does. */
165
+ declare(type: Type): void;
143
166
  /**
144
167
  * The Zod-free type for a single property - used for path and query parameters, which are
145
168
  * `ModelProperty`s belonging to no model. Without routing them through the registry a named model
@@ -147,7 +170,16 @@ export declare class TypeRegistry {
147
170
  */
148
171
  /** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
149
172
  expressionForProperty(property: ModelProperty, wireName?: string): string;
173
+ /** A multipart body's type, under the component's name - see {@link SchemaRegistry.declareNamed}. */
174
+ declareNamed(name: string, build: () => string, site: Type): string;
150
175
  /** Every declaration, dependencies first. */
151
176
  declarations(): readonly TsDeclaration[];
177
+ /**
178
+ * **A contract type whose validator validates nothing is `never` too**, whatever refused it: two
179
+ * shapes under one name, a union this emitter refuses, a model openapi3 publishes split. The wire
180
+ * assertion pairs the two by name, and a contract type stating a shape beside `z.never()` failed
181
+ * it - `interface HBase { name: string }` beside a refused `hBaseSchema`.
182
+ */
183
+ refuse(names: ReadonlySet<string>): void;
152
184
  }
153
185
  export {};