typespec-http-zod 0.17.0 → 0.18.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 +67 -21
- package/package.json +1 -1
package/dist/src/api.js
CHANGED
|
@@ -617,7 +617,13 @@ function alternateSchemaFor(operation, status, registry) {
|
|
|
617
617
|
function multipartTsOf(body, registry) {
|
|
618
618
|
const entries = body.parts.map((part) => {
|
|
619
619
|
const type = part.body?.type;
|
|
620
|
-
const inner = type === undefined
|
|
620
|
+
const inner = type === undefined
|
|
621
|
+
? "unknown"
|
|
622
|
+
: isFilePart(type)
|
|
623
|
+
? MULTIPART_FILE
|
|
624
|
+
: isBinaryPart(type)
|
|
625
|
+
? "unknown"
|
|
626
|
+
: registry.expressionFor(type);
|
|
621
627
|
const value = part.multi === true ? `(${inner})[]` : inner;
|
|
622
628
|
const optional = part.optional === true ? "?" : "";
|
|
623
629
|
return `\t${objectKey(part.name ?? "")}${optional}: ${value};`;
|
|
@@ -627,7 +633,13 @@ function multipartTsOf(body, registry) {
|
|
|
627
633
|
function multipartSchemaOf(body, registry) {
|
|
628
634
|
const entries = body.parts.map((part) => {
|
|
629
635
|
const type = part.body?.type;
|
|
630
|
-
const inner = type === undefined
|
|
636
|
+
const inner = type === undefined
|
|
637
|
+
? "z.unknown()"
|
|
638
|
+
: isFilePart(type)
|
|
639
|
+
? `z.unknown().refine(${MULTIPART_FILE_REFINEMENT})`
|
|
640
|
+
: isBinaryPart(type)
|
|
641
|
+
? "z.unknown()"
|
|
642
|
+
: registry.expressionFor(type);
|
|
631
643
|
// `multi` is `HttpPart<T>[]` - the same part repeated, which arrives as an array.
|
|
632
644
|
const value = part.multi === true ? `z.array(${inner})` : inner;
|
|
633
645
|
return `\t${objectKey(part.name ?? "")}: ${part.optional === true ? `${value}.optional()` : value},`;
|
|
@@ -643,26 +655,60 @@ function multipartSchemaOf(body, registry) {
|
|
|
643
655
|
return name === undefined ? shape : registry.declareNamed(name, () => shape);
|
|
644
656
|
}
|
|
645
657
|
/**
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* **
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* -
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
* The
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
658
|
+
* The shape a multipart file part actually arrives as, spelled structurally.
|
|
659
|
+
*
|
|
660
|
+
* **Structural rather than `File`, because emitted output must depend on no ambient library.**
|
|
661
|
+
* `File` is reachable only through `lib.dom` or `@types/node`, and this package's own
|
|
662
|
+
* emitted-output typecheck runs with `types: []` precisely so a dependency like that cannot creep
|
|
663
|
+
* in. The three members are the ones a handler needs and the ones {@link MULTIPART_FILE_REFINEMENT}
|
|
664
|
+
* actually verifies - claiming more would be asserting what nothing established.
|
|
665
|
+
*
|
|
666
|
+
* `arrayBuffer` is a property rather than a method on purpose: `Declared<>` maps the inferred side
|
|
667
|
+
* of the wire assertion, and a mapped type turns a method into a function-typed property, so
|
|
668
|
+
* spelling it this way on both sides is what keeps the two identical.
|
|
669
|
+
*/
|
|
670
|
+
const MULTIPART_FILE = "{ name: string; type: string; arrayBuffer: () => Promise<ArrayBuffer> }";
|
|
671
|
+
/**
|
|
672
|
+
* The check that makes {@link MULTIPART_FILE} established rather than asserted.
|
|
673
|
+
*
|
|
674
|
+
* **Why this is derivable from the contract, which is the rule `test/vocabulary.test.ts` enforces.**
|
|
675
|
+
* `@typespec/openapi3` publishes a bare `{}` for a `HttpPart<File>`, in 3.1 and even where the part
|
|
676
|
+
* declares a content type. That is OpenAPI's IDIOM for binary content in a multipart body rather
|
|
677
|
+
* than a statement that any value is acceptable, and the transport says the same: Hono types a
|
|
678
|
+
* multipart part as `string | File` and nothing else. So this refuses exactly one thing - a text
|
|
679
|
+
* field where the spec declared a file - which is malformed against the spec that produced the
|
|
680
|
+
* document. A spec meaning "either" writes `HttpPart<File | string>`, so nothing becomes
|
|
681
|
+
* inexpressible; the declaration simply means what it says.
|
|
682
|
+
*
|
|
683
|
+
* **`z.unknown().refine()` rather than `z.custom<T>()`, and the difference is not stylistic.**
|
|
684
|
+
* Measured on zod 4.4.3: `z.toJSONSchema(z.custom(...))` throws `Custom types cannot be represented
|
|
685
|
+
* in JSON Schema`, which silently took four corpus schemas out of the differential comparing these
|
|
686
|
+
* validators against the document. Refined `z.unknown()` serialises to `{}` - exactly what openapi3
|
|
687
|
+
* publishes - so that comparison keeps working. Zod 4 narrows on a type predicate, so the inferred
|
|
688
|
+
* type is the same either way.
|
|
689
|
+
*
|
|
690
|
+
* Every member the type claims is verified, so the narrowing is sound.
|
|
665
691
|
*/
|
|
692
|
+
const MULTIPART_FILE_REFINEMENT = `(value): value is ${MULTIPART_FILE} => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string" && "arrayBuffer" in value && typeof value.arrayBuffer === "function"`;
|
|
693
|
+
/**
|
|
694
|
+
* A part declared as a `File`, as opposed to raw `bytes`.
|
|
695
|
+
*
|
|
696
|
+
* **Narrower than {@link isBinaryPart} deliberately.** A `File` part is declared to be a file and
|
|
697
|
+
* `c.req.parseBody()` produces one, so the contract supports saying so. `bytes` says only that the
|
|
698
|
+
* payload is binary; a client may legitimately send it as an ordinary form field, which arrives as a
|
|
699
|
+
* string. Widening this to cover `bytes` would refuse payloads that pass today, on an inference the
|
|
700
|
+
* spec never made.
|
|
701
|
+
*/
|
|
702
|
+
function isFilePart(type) {
|
|
703
|
+
if (type.kind !== "Model")
|
|
704
|
+
return false;
|
|
705
|
+
for (let model = type; model !== undefined; model = model.baseModel) {
|
|
706
|
+
if (model.name === "File")
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
/** `bytes`, or a model built on `File` - the parts openapi3 publishes as an empty schema. */
|
|
666
712
|
function isBinaryPart(type) {
|
|
667
713
|
if (type.kind === "Scalar") {
|
|
668
714
|
for (let scalar = type; scalar !== undefined; scalar = scalar.baseScalar) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|