typespec-http-zod 0.3.0 → 0.4.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/README.md CHANGED
@@ -146,6 +146,8 @@ carries the whole id, so `op readWidget` in `namespace Widgets` becomes `Widgets
146
146
  - [Guides](docs/guides.md): installing, validating a request, answering with the right body, content
147
147
  types, and building an emitter on the API.
148
148
  - [Reference](docs/reference.md): every option, every diagnostic, and the known limits.
149
+ - [Oracles](docs/oracles.md): every artefact this emitter produces, and what compares it to the
150
+ thing it has to agree with.
149
151
 
150
152
  ## Licence
151
153
 
package/dist/src/api.js CHANGED
@@ -517,6 +517,20 @@ const COLLECTION_DELIMITERS = {
517
517
  function isCollectionParameter(property) {
518
518
  return isArrayType(property.type);
519
519
  }
520
+ /**
521
+ * Whether the values arrive one per OCCURRENCE (RFC 6570 explode) rather than joined into one.
522
+ *
523
+ * The distinction matters because of what `zValidator` hands the schema: for a repeated key it
524
+ * builds an array, but for a single occurrence it hands the bare string - and one `key=value` pair
525
+ * is exactly what a one-member exploded array looks like on the wire. A bare `z.array()` therefore
526
+ * refused a conformant request while accepting the same list sent twice, which no document
527
+ * describes. Found by the first consumer's `?topics=ip` (#1).
528
+ */
529
+ function isExplodedCollection(parameter) {
530
+ return (isCollectionParameter(parameter.param) &&
531
+ (parameter.type === "query" || parameter.type === "header") &&
532
+ parameter.explode === true);
533
+ }
520
534
  /**
521
535
  * The delimiter a parameter's values were joined with, or `undefined` when they were not joined.
522
536
  *
@@ -675,9 +689,18 @@ function parameterSchemasOf(program, operation, registry) {
675
689
  const decoded = parameter.type === "header" && parameter.name.toLowerCase() === "content-type"
676
690
  ? mediaTypeDecoded(declared)
677
691
  : wireDecoded(declared);
678
- const expression = delimiter === undefined
679
- ? decoded
680
- : `z.preprocess((raw) => (typeof raw === "string" ? raw.split(${JSON.stringify(delimiter)}) : raw), ${decoded})`;
692
+ /**
693
+ * The boxing half of the same principle: an exploded array's single occurrence arrives as a
694
+ * bare string, so it is boxed into the one-element array the document describes before the
695
+ * schema runs - every constraint on the list still applies to the result. Several
696
+ * occurrences arrive already an array and pass through untouched, exactly as the delimiter
697
+ * arm leaves a pre-split value alone.
698
+ */
699
+ const expression = isExplodedCollection(parameter)
700
+ ? `z.preprocess((raw) => (typeof raw === "string" ? [raw] : raw), ${decoded})`
701
+ : delimiter === undefined
702
+ ? decoded
703
+ : `z.preprocess((raw) => (typeof raw === "string" ? raw.split(${JSON.stringify(delimiter)}) : raw), ${decoded})`;
681
704
  const entry = `\t${objectKey(parameter.name)}: ${expression},`;
682
705
  (byTarget[parameter.type] ??= []).push(entry);
683
706
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-http-zod",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",