typespec-hono 0.19.1 → 0.20.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/app.js CHANGED
@@ -512,7 +512,49 @@ type Fields<T> = string extends keyof T ? ([T[string]] extends [never] ? unknown
512
512
  * here on its own, because this signature is derived from `z.infer` rather than from those
513
513
  * types - which is exactly the half-fix `test/openmodel/` exists to catch.
514
514
  */
515
- const output = names.response === undefined ? "void" : `Produced<z.infer<typeof ${names.response}>>`;
515
+ /**
516
+ * **The ENVELOPE a handler has to be able to say, beside the body it returns.**
517
+ *
518
+ * `@statusCode` and `@header` properties are stripped from the body schema - correctly, they
519
+ * are not body - so a return type derived from that schema alone could not carry them. The arms
520
+ * name them anyway: `{ headers: [{ property: "correlationId" }] }` tells `respond` to read a
521
+ * property off the returned value, and `when: { property: "statusCode" }` tells it which arm
522
+ * the handler meant. Measured before this existed, on `payload__head`:
523
+ * `Awaitable<Result<void>>` against an arm naming two header properties, so the emitter
524
+ * published an envelope contract nothing could satisfy.
525
+ *
526
+ * Only the SUCCESS statuses count. `responseHeaders` covers error responses too, and a header
527
+ * declared on a 404 is the error body's business rather than something a handler returns.
528
+ */
529
+ const successStatuses = route.statusSelector?.statuses ?? [route.statusCode];
530
+ const envelope = [];
531
+ if (route.statusSelector !== undefined) {
532
+ envelope.push(`${objectKey(route.statusSelector.property)}: ${route.statusSelector.statuses.join(" | ")}`);
533
+ }
534
+ const headerEntries = route.responseHeaders.filter((entry) => successStatuses.includes(entry.status));
535
+ const declaredOn = new Map();
536
+ for (const entry of headerEntries) {
537
+ for (const header of entry.headers) {
538
+ const seen = declaredOn.get(header.property);
539
+ declaredOn.set(header.property, {
540
+ count: (seen?.count ?? 0) + 1,
541
+ type: header.type,
542
+ });
543
+ }
544
+ }
545
+ for (const [property, { count, type }] of declaredOn) {
546
+ // Required only where EVERY success status declares it; otherwise the handler cannot know
547
+ // which status it is answering with until it has chosen one.
548
+ const optional = count === headerEntries.length && headerEntries.length === successStatuses.length;
549
+ envelope.push(`${objectKey(property)}${optional ? "" : "?"}: ${type}`);
550
+ }
551
+ const envelopeType = envelope.length === 0 ? undefined : `{ ${envelope.join("; ")} }`;
552
+ const body = names.response === undefined ? undefined : `Produced<z.infer<typeof ${names.response}>>`;
553
+ const output = body === undefined
554
+ ? (envelopeType ?? "void")
555
+ : envelopeType === undefined
556
+ ? body
557
+ : `${body} & ${envelopeType}`;
516
558
  const signature = `ctx: Ctx, input: ${input ?? EMPTY_INPUT}`;
517
559
  const doc = route.summary === undefined ? "" : `\t/** ${route.summary} */\n`;
518
560
  return `${doc}\t${route.operationId}(${signature}): Awaitable<Result<${output}>>;`;
@@ -36,7 +36,15 @@ export interface ResponseArm {
36
36
  }[];
37
37
  readonly when?: {
38
38
  readonly property: string;
39
- readonly value: boolean | string;
39
+ /**
40
+ * **A number too, because a `@statusCode` union selects by the status itself.**
41
+ *
42
+ * `model Created { @statusCode statusCode: 200 | 201 }` names the property that chooses, and
43
+ * its values are the statuses. The discriminator case carries a boolean or string literal off
44
+ * the body instead; both are the same question - which arm did the handler mean - so both use
45
+ * this one field.
46
+ */
47
+ readonly value: boolean | number | string;
40
48
  };
41
49
  }
42
50
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-hono",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition, agreeing with the OpenAPI document @typespec/openapi3 publishes from the same source.",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -44,7 +44,7 @@
44
44
  "provenance": true
45
45
  },
46
46
  "dependencies": {
47
- "typespec-http-zod": "^0.21.0"
47
+ "typespec-http-zod": "^0.22.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@hono/zod-openapi": "^1.4.0",
package/src/runtime.ts CHANGED
@@ -34,7 +34,15 @@ export interface ResponseArm {
34
34
  readonly headers?: readonly { readonly name: string; readonly property: string }[];
35
35
  readonly when?: {
36
36
  readonly property: string;
37
- readonly value: boolean | string;
37
+ /**
38
+ * **A number too, because a `@statusCode` union selects by the status itself.**
39
+ *
40
+ * `model Created { @statusCode statusCode: 200 | 201 }` names the property that chooses, and
41
+ * its values are the statuses. The discriminator case carries a boolean or string literal off
42
+ * the body instead; both are the same question - which arm did the handler mean - so both use
43
+ * this one field.
44
+ */
45
+ readonly value: boolean | number | string;
38
46
  };
39
47
  }
40
48