typespec-http-zod 0.2.0 → 0.3.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.js CHANGED
@@ -1081,22 +1081,42 @@ function collectRequestTypes(program, registry, service) {
1081
1081
  */
1082
1082
  const extraProperties = operation.parameters.parameters
1083
1083
  .filter((parameter) => parameter.type === "path" || parameter.type === "query" || parameter.type === "header")
1084
- .map((parameter) => registry.expressionForProperty(parameter.param));
1084
+ /**
1085
+ * Named as the WIRE names it, which is what the emitted validator keys on.
1086
+ * `@header("x-thing") thing: string` is `"x-thing"` in `schemas.gen.ts`, so it has to be
1087
+ * `"x-thing"` here too: a consumer checking its handlers against these types otherwise
1088
+ * fails to typecheck against a server that is correct.
1089
+ */
1090
+ .map((parameter) => registry.expressionForProperty(parameter.param, parameter.name));
1085
1091
  if (body === undefined && extraProperties.length === 0)
1086
1092
  continue;
1087
1093
  /**
1088
- * A `bytes` body reaches the use-case as a NAMED property holding the unparsed text, not as
1089
- * a bare string - so the wire type is `{ rawBody: string }`, matching what a server
1090
- * actually builds. Referencing the scalar directly would type the whole input as `string`
1091
- * and quietly drop the headers beside it.
1094
+ * A `bytes` body reaches the use-case as a NAMED property holding the unparsed body, not as
1095
+ * a bare value, so the wire type is `{ rawBody: ... }`, matching what a server actually
1096
+ * builds. Referencing the scalar directly would type the whole input as that value and
1097
+ * quietly drop the headers beside it.
1092
1098
  */
1093
1099
  const rawBody = body?.kind === "Scalar" && body.name === "bytes"
1094
1100
  ? (operation.parameters.body?.property?.name ?? "body")
1095
1101
  : undefined;
1102
+ /**
1103
+ * **Raw binary is `ArrayBuffer`; base64 inside a JSON payload is `string`.** The same
1104
+ * `isRawBinaryMediaType` rule the response side and the multipart parts already follow,
1105
+ * applied here for the first time.
1106
+ *
1107
+ * This type said `string` for every `bytes` body whatever it was served as. A server reading
1108
+ * an `application/octet-stream` body hands the handler bytes, so a consumer checking its
1109
+ * handlers against these types could not typecheck against a correct server. It is the
1110
+ * request half of the corruption already fixed on the reader: decoding raw bytes as text
1111
+ * replaces every byte outside ASCII, and typing them as text is what invites it.
1112
+ */
1113
+ const rawBodyType = isRawBinaryMediaType([...(operation.parameters.body?.contentTypes ?? [])])
1114
+ ? "ArrayBuffer"
1115
+ : "string";
1096
1116
  entries.push({
1097
1117
  operationId: resolveOperationId(program, operation.operation),
1098
1118
  bodyRef: rawBody !== undefined
1099
- ? `{\n\t${rawBody}: string;\n}`
1119
+ ? `{\n\t${rawBody}: ${rawBodyType};\n}`
1100
1120
  : body === undefined
1101
1121
  ? undefined
1102
1122
  : registry.expressionFor(body),
@@ -112,7 +112,8 @@ export declare class TypeRegistry {
112
112
  * `ModelProperty`s belonging to no model. Without routing them through the registry a named model
113
113
  * or enum in a parameter position is inlined rather than referenced.
114
114
  */
115
- expressionForProperty(property: ModelProperty): string;
115
+ /** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
116
+ expressionForProperty(property: ModelProperty, wireName?: string): string;
116
117
  /** Every declaration, dependencies first. */
117
118
  declarations(): readonly TsDeclaration[];
118
119
  }
@@ -404,12 +404,13 @@ export class TypeRegistry {
404
404
  * `ModelProperty`s belonging to no model. Without routing them through the registry a named model
405
405
  * or enum in a parameter position is inlined rather than referenced.
406
406
  */
407
- expressionForProperty(property) {
407
+ /** `wireName` names an HTTP parameter as the wire does - see {@link propertyToTs}. */
408
+ expressionForProperty(property, wireName) {
408
409
  return withTsRefResolver((candidate) => {
409
410
  if (declaredNameOf(candidate) === undefined)
410
411
  return undefined;
411
412
  return this.expressionFor(candidate);
412
- }, () => propertyToTs(this.#program, property));
413
+ }, () => propertyToTs(this.#program, property, wireName));
413
414
  }
414
415
  #inline(type) {
415
416
  return withTsRefResolver((candidate) => {
@@ -1,6 +1,15 @@
1
1
  import { type ModelProperty, type Program, type Type } from "@typespec/compiler";
2
- /** A property line, including its optionality marker. */
3
- export declare function propertyToTs(program: Program, property: ModelProperty): string;
2
+ /**
3
+ * A property line, including its optionality marker.
4
+ *
5
+ * `wireName` overrides the key for an HTTP PARAMETER, whose wire name comes from `@header("x-thing")`
6
+ * or `@query("$select")` rather than from any JSON encoding. `resolveEncodedName` cannot see those,
7
+ * so a header renamed on the wire was keyed here by its TypeScript property name while the emitted
8
+ * validator keyed it by the wire name. A consumer checking its handlers against the generated input
9
+ * types then failed to typecheck against a server that was correct: `z.object({ "x-thing": ... })`
10
+ * on one side and `{ thing: string }` on the other.
11
+ */
12
+ export declare function propertyToTs(program: Program, property: ModelProperty, wireName?: string): string;
4
13
  export declare function withTsRefResolver<T>(resolver: (type: Type) => string | undefined, run: () => T): T;
5
14
  export declare function typeToTs(program: Program, type: Type): string;
6
15
  /**
package/dist/src/types.js CHANGED
@@ -199,11 +199,20 @@ function modelToTs(program, model) {
199
199
  */
200
200
  return entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
201
201
  }
202
- /** A property line, including its optionality marker. */
203
- export function propertyToTs(program, property) {
202
+ /**
203
+ * A property line, including its optionality marker.
204
+ *
205
+ * `wireName` overrides the key for an HTTP PARAMETER, whose wire name comes from `@header("x-thing")`
206
+ * or `@query("$select")` rather than from any JSON encoding. `resolveEncodedName` cannot see those,
207
+ * so a header renamed on the wire was keyed here by its TypeScript property name while the emitted
208
+ * validator keyed it by the wire name. A consumer checking its handlers against the generated input
209
+ * types then failed to typecheck against a server that was correct: `z.object({ "x-thing": ... })`
210
+ * on one side and `{ thing: string }` on the other.
211
+ */
212
+ export function propertyToTs(program, property, wireName) {
204
213
  // The WIRE name, resolved exactly as `zod.ts` resolves it - `wire-contract.gen.ts` asserts the
205
214
  // emitted Zod infers this type, so the two must name every property identically or it fails.
206
- const key = objectKey(resolveEncodedName(program, property, "application/json"));
215
+ const key = objectKey(wireName ?? resolveEncodedName(program, property, "application/json"));
207
216
  // `@encode` changes the wire TYPE, resolved exactly as `zod.ts` resolves it - a property encoded as
208
217
  // a string on one side and an array on the other fails the emitted assertion, and deserves to.
209
218
  const encoded = getEncode(program, property)?.type;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",