typespec-http-zod 0.3.0 → 0.5.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 +2 -0
- package/dist/src/api.js +48 -4
- package/dist/src/lib.d.ts +3 -3
- package/dist/src/lib.js +3 -3
- package/dist/src/types.js +20 -10
- package/dist/src/zod.d.ts +8 -0
- package/dist/src/zod.js +53 -11
- package/package.json +1 -1
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
/**
|
|
@@ -1366,11 +1389,32 @@ export async function emitHttpZod(context, wrapper = {}) {
|
|
|
1366
1389
|
* carrying internal models if you are not paying attention. It is invisible while there is one
|
|
1367
1390
|
* service, which is exactly why it is built this way before the second one lands rather than after.
|
|
1368
1391
|
*/
|
|
1392
|
+
/**
|
|
1393
|
+
* **Two `@service` namespaces wrote one `schemas.gen.ts`, and the second silently won.**
|
|
1394
|
+
*
|
|
1395
|
+
* Every service emitted into `emitterOutputDir` under the same file name, so a spec publishing an
|
|
1396
|
+
* internal surface and a public one produced a single file holding whichever was walked last.
|
|
1397
|
+
* Measured on two services with no per-service `output-dir`: zero diagnostics, one file, and the
|
|
1398
|
+
* first service's validators absent entirely. Nothing refused, nothing warned - a consumer would
|
|
1399
|
+
* find half their API had no validators at all.
|
|
1400
|
+
*
|
|
1401
|
+
* The rule is `@typespec/openapi3`'s, copied: its default output path is
|
|
1402
|
+
* `openapi.{service-name-if-multiple}...`, so a name is inserted only when there is more than one
|
|
1403
|
+
* service to tell apart. A single-service spec emits exactly where it always did.
|
|
1404
|
+
*
|
|
1405
|
+
* The directory is what carries it rather than the file name, because `EmittedService.outputDir`
|
|
1406
|
+
* is already the published seam a wrapping emitter writes its own files beside. Disambiguating
|
|
1407
|
+
* there means a server emitter follows without knowing this rule exists.
|
|
1408
|
+
*/
|
|
1409
|
+
const distinctServices = new Set(snapshots.map((snapshot) => snapshot.service.namespace.name));
|
|
1369
1410
|
for (const snapshot of snapshots) {
|
|
1370
1411
|
const service = snapshot.service;
|
|
1371
1412
|
const serviceName = service.namespace.name;
|
|
1372
1413
|
const perService = options.services?.[serviceName];
|
|
1373
|
-
const outputDir = perService?.["output-dir"] ??
|
|
1414
|
+
const outputDir = perService?.["output-dir"] ??
|
|
1415
|
+
(distinctServices.size > 1
|
|
1416
|
+
? resolvePath(context.emitterOutputDir, serviceName)
|
|
1417
|
+
: context.emitterOutputDir);
|
|
1374
1418
|
const resolved = {
|
|
1375
1419
|
contractsOutputDir: perService?.["contracts-output-dir"] ?? options["contracts-output-dir"],
|
|
1376
1420
|
contractsPackage: perService?.["contracts-package"] ?? options["contracts-package"],
|
package/dist/src/lib.d.ts
CHANGED
|
@@ -104,7 +104,7 @@ export interface EmitterOptions {
|
|
|
104
104
|
export declare const EmitterOptionsSchema: JSONSchemaType<EmitterOptions>;
|
|
105
105
|
export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
106
106
|
"unsupported-type": {
|
|
107
|
-
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why"]>;
|
|
107
|
+
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
108
108
|
};
|
|
109
109
|
"unsupported-default": {
|
|
110
110
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
@@ -124,7 +124,7 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
124
124
|
}, EmitterOptions, never>;
|
|
125
125
|
export declare const reportDiagnostic: <C extends "empty-union" | "undeclared-discriminator" | "unknown-key-vocabulary" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
126
126
|
"unsupported-type": {
|
|
127
|
-
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why"]>;
|
|
127
|
+
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
128
128
|
};
|
|
129
129
|
"unsupported-default": {
|
|
130
130
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
@@ -143,7 +143,7 @@ export declare const reportDiagnostic: <C extends "empty-union" | "undeclared-di
|
|
|
143
143
|
};
|
|
144
144
|
}[C]>(program: import("@typespec/compiler").Program, diag: import("@typespec/compiler").DiagnosticReport<{
|
|
145
145
|
"unsupported-type": {
|
|
146
|
-
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why"]>;
|
|
146
|
+
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
147
147
|
};
|
|
148
148
|
"unsupported-default": {
|
|
149
149
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
package/dist/src/lib.js
CHANGED
|
@@ -65,13 +65,13 @@ const diagnostics = {
|
|
|
65
65
|
"unsupported-type": {
|
|
66
66
|
severity: "error",
|
|
67
67
|
messages: {
|
|
68
|
-
default: paramMessage `Cannot emit ${"artefact"} for ${"kind"}: ${"why"}
|
|
68
|
+
default: paramMessage `Cannot emit ${"artefact"} for ${"kind"}: ${"why"}. Replace it with a type a value can inhabit. ${"remedy"}`,
|
|
69
69
|
},
|
|
70
70
|
},
|
|
71
71
|
"unsupported-default": {
|
|
72
72
|
severity: "error",
|
|
73
73
|
messages: {
|
|
74
|
-
default: paramMessage `Cannot emit a default value of ${"why"}. Scalars, arrays and objects are emitted as literals; a default built by a scalar constructor has no literal form. The property keeps its declared shape and loses only the fallback.`,
|
|
74
|
+
default: paramMessage `Cannot emit a default value of ${"why"}. Scalars, arrays and objects are emitted as literals; a default built by a scalar constructor has no literal form. Replace it with a literal value, or drop the default and let the property be optional. The property keeps its declared shape and loses only the fallback.`,
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
77
|
/**
|
|
@@ -107,7 +107,7 @@ const diagnostics = {
|
|
|
107
107
|
"empty-union": {
|
|
108
108
|
severity: "error",
|
|
109
109
|
messages: {
|
|
110
|
-
default: paramMessage `A union with no representable variants (${"why"}) has nothing to validate against.`,
|
|
110
|
+
default: paramMessage `A union with no representable variants (${"why"}) has nothing to validate against. Declare at least one variant this emitter can walk, or replace the union with the single type it reduces to.`,
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
113
|
/*
|
package/dist/src/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDiscriminatedUnion, getEncode, isNeverType, resolveEncodedName, } from "@typespec/compiler";
|
|
2
2
|
import { reportDiagnostic } from "./lib.js";
|
|
3
|
-
import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, objectKey, } from "./zod.js";
|
|
3
|
+
import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, objectKey, refusalTarget, withDeclarationSite, } from "./zod.js";
|
|
4
4
|
/**
|
|
5
5
|
* TypeSpec type -> plain TypeScript type text.
|
|
6
6
|
*
|
|
@@ -53,12 +53,17 @@ const SCALARS = {
|
|
|
53
53
|
* built on one cannot be used by accident, and the error diagnostic has already failed the compile.
|
|
54
54
|
*/
|
|
55
55
|
const UNREPRESENTABLE = "never";
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Refuse `type`, pointing at somewhere the reader can go, and carry on so the rest is reported too.
|
|
58
|
+
*
|
|
59
|
+
* An intrinsic has no source node, so it is located by the declaration being walked instead - see
|
|
60
|
+
* `withDeclarationSite` in `zod.ts`, whose scope this walk shares.
|
|
61
|
+
*/
|
|
62
|
+
function refuse(program, type, why, remedy = "") {
|
|
58
63
|
reportDiagnostic(program, {
|
|
59
64
|
code: "unsupported-type",
|
|
60
|
-
target: type,
|
|
61
|
-
format: { artefact: "a TypeScript type", kind: type.kind, why },
|
|
65
|
+
target: refusalTarget(type),
|
|
66
|
+
format: { artefact: "a TypeScript type", kind: type.kind, why, remedy },
|
|
62
67
|
});
|
|
63
68
|
return UNREPRESENTABLE;
|
|
64
69
|
}
|
|
@@ -166,7 +171,12 @@ function modelToTs(program, model) {
|
|
|
166
171
|
* as a bare `Record<string, unknown>` - a type that satisfies any assertion about what the response
|
|
167
172
|
* contains, which is precisely the check this file exists to feed.
|
|
168
173
|
*/
|
|
169
|
-
|
|
174
|
+
// `Record<never>` states closedness rather than a value type - see `modelToZod`. A model that is
|
|
175
|
+
// only that is an object with no declared properties, not a dictionary of an unwalkable type.
|
|
176
|
+
if (indexerKey === "string" &&
|
|
177
|
+
indexer !== undefined &&
|
|
178
|
+
!isNeverType(indexer.value) &&
|
|
179
|
+
declared.length === 0) {
|
|
170
180
|
return `Record<string, ${typeToTs(program, indexer.value)}>`;
|
|
171
181
|
}
|
|
172
182
|
/**
|
|
@@ -185,7 +195,7 @@ function modelToTs(program, model) {
|
|
|
185
195
|
*/
|
|
186
196
|
const entries = declared
|
|
187
197
|
.filter((property) => !isNeverType(property.type))
|
|
188
|
-
.map((property) => `\t${propertyToTs(program, property)}`);
|
|
198
|
+
.map((property) => `\t${withDeclarationSite(property, () => propertyToTs(program, property))}`);
|
|
189
199
|
/**
|
|
190
200
|
* **The indexer is deliberately NOT intersected in.**
|
|
191
201
|
*
|
|
@@ -279,10 +289,10 @@ export function typeToTsBody(program, type) {
|
|
|
279
289
|
if (type.name === "unknown")
|
|
280
290
|
return "unknown";
|
|
281
291
|
if (type.name === "void" || type.name === "never") {
|
|
282
|
-
return refuse(program, type, `\`${type.name}\` has no runtime representation`);
|
|
292
|
+
return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never` PROPERTY is dropped to match the document, and `Record<never>` seals the model, so neither of those reaches here.");
|
|
283
293
|
}
|
|
284
|
-
return refuse(program, type, `intrinsic "${type.name}"
|
|
294
|
+
return refuse(program, type, `intrinsic "${type.name}"`, "This emitter has no rule for that intrinsic. Replace it with a declared scalar or model.");
|
|
285
295
|
default:
|
|
286
|
-
return refuse(program, type, "no rule for this kind");
|
|
296
|
+
return refuse(program, type, "no rule for this kind", "Replace it with a model, scalar, enum or union, which are the kinds this emitter walks.");
|
|
287
297
|
}
|
|
288
298
|
}
|
package/dist/src/zod.d.ts
CHANGED
|
@@ -52,6 +52,14 @@ export declare function collectExternalImports<T>(walk: () => T): {
|
|
|
52
52
|
readonly imports: Map<string, Set<string>>;
|
|
53
53
|
readonly vocabularies: Map<string, readonly string[]>;
|
|
54
54
|
};
|
|
55
|
+
export declare function withDeclarationSite<T>(site: Type | undefined, run: () => T): T;
|
|
56
|
+
/**
|
|
57
|
+
* Where a refusal should point.
|
|
58
|
+
*
|
|
59
|
+
* The offending type, when it has a source location of its own. Otherwise the declaration being
|
|
60
|
+
* walked, which does.
|
|
61
|
+
*/
|
|
62
|
+
export declare function refusalTarget(type: Type): Type;
|
|
55
63
|
export declare function withSyntheticDeclarations<T>(hook: (name: string, build: () => string) => string, run: () => T): T;
|
|
56
64
|
/**
|
|
57
65
|
* Every property the model declares, **including the ones it inherits**.
|
package/dist/src/zod.js
CHANGED
|
@@ -275,12 +275,43 @@ export function collectExternalImports(walk) {
|
|
|
275
275
|
vocabularies = previousVocabularies;
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The declaration currently being walked, used to locate a refusal that has nowhere of its own.
|
|
280
|
+
*
|
|
281
|
+
* An intrinsic like `never` has no source node, so pointing a diagnostic at it prints
|
|
282
|
+
* `<unknown location>:1:1`. A consumer compiling a large spec got 32 byte-identical messages with
|
|
283
|
+
* nothing to grep for and no way to tell which declaration caused which. The property or model being
|
|
284
|
+
* walked when the intrinsic was reached is the thing they can actually go and edit.
|
|
285
|
+
*
|
|
286
|
+
* Scoped rather than threaded, matching the other ambient facts in this walk.
|
|
287
|
+
*/
|
|
288
|
+
let declarationSite;
|
|
289
|
+
export function withDeclarationSite(site, run) {
|
|
290
|
+
const previous = declarationSite;
|
|
291
|
+
if (site !== undefined)
|
|
292
|
+
declarationSite = site;
|
|
293
|
+
try {
|
|
294
|
+
return run();
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
declarationSite = previous;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Where a refusal should point.
|
|
302
|
+
*
|
|
303
|
+
* The offending type, when it has a source location of its own. Otherwise the declaration being
|
|
304
|
+
* walked, which does.
|
|
305
|
+
*/
|
|
306
|
+
export function refusalTarget(type) {
|
|
307
|
+
return type.node === undefined ? (declarationSite ?? type) : type;
|
|
308
|
+
}
|
|
278
309
|
/** Refuse `type`, pointing at its declaration, and carry on so the rest is reported too. */
|
|
279
|
-
function refuse(program, type, why) {
|
|
310
|
+
function refuse(program, type, why, remedy = "") {
|
|
280
311
|
reportDiagnostic(program, {
|
|
281
312
|
code: "unsupported-type",
|
|
282
|
-
target: type,
|
|
283
|
-
format: { artefact: "Zod", kind: type.kind, why },
|
|
313
|
+
target: refusalTarget(type),
|
|
314
|
+
format: { artefact: "Zod", kind: type.kind, why, remedy },
|
|
284
315
|
});
|
|
285
316
|
return UNREPRESENTABLE;
|
|
286
317
|
}
|
|
@@ -474,7 +505,7 @@ function unionToZod(program, union) {
|
|
|
474
505
|
if (discriminated.defaultVariant !== undefined) {
|
|
475
506
|
// "Anything else" has no discriminator value to switch on. Refused by name rather than
|
|
476
507
|
// silently dropped - no corpus scenario exercises it, so guessing would be untested code.
|
|
477
|
-
return refuse(program, union, "a discriminated union with a default variant");
|
|
508
|
+
return refuse(program, union, "a discriminated union with a default variant", "Remove the default variant, or declare it as an ordinary variant with its own discriminator value.");
|
|
478
509
|
}
|
|
479
510
|
/**
|
|
480
511
|
* **Refused, not compensated for.** With no envelope the discriminator lives inside the
|
|
@@ -633,7 +664,7 @@ function modelToZod(program, model) {
|
|
|
633
664
|
* it once keyed on the model's *name*, and lost the indexer of anything derived from `Record`.
|
|
634
665
|
*/
|
|
635
666
|
if (indexerKey === "string" && indexer !== undefined && declared.length === 0) {
|
|
636
|
-
return applyConstraints(program, `z.record(z.string(), ${typeToZod(program, indexer.value)})`, model);
|
|
667
|
+
return applyConstraints(program, `z.record(z.string(), ${withDeclarationSite(model, () => typeToZod(program, indexer.value))})`, model);
|
|
637
668
|
}
|
|
638
669
|
/**
|
|
639
670
|
* Permissiveness, read from the INDEXER and from nothing else.
|
|
@@ -646,12 +677,23 @@ function modelToZod(program, model) {
|
|
|
646
677
|
* An indexer over something other than `unknown` is a typed catchall, where `.loose()` would drop
|
|
647
678
|
* that value type on the floor.
|
|
648
679
|
*/
|
|
649
|
-
|
|
680
|
+
/**
|
|
681
|
+
* **`Record<never>` is how a spec says "and nothing else", not a value type to walk.**
|
|
682
|
+
*
|
|
683
|
+
* `model A { ...Record<never>; name: string }` gives an indexer whose value is the `never`
|
|
684
|
+
* intrinsic. Read as an ordinary typed catchall it became `.catchall(z.never())`, which reached
|
|
685
|
+
* `typeToZod` and refused the whole compile - while `@typespec/openapi3` publishes the same model
|
|
686
|
+
* cleanly as `additionalProperties: {not: {}}`, with no diagnostic at all. `isSealed` already reads
|
|
687
|
+
* a `never` indexer as sealed; this stops the catchall branch consuming it first.
|
|
688
|
+
*/
|
|
689
|
+
const indexerValue = indexerKey === "string" && indexer !== undefined && !isNeverType(indexer.value)
|
|
690
|
+
? indexer.value
|
|
691
|
+
: undefined;
|
|
650
692
|
const suffix = indexerValue !== undefined && isUnknownType(indexerValue)
|
|
651
693
|
? ".loose()"
|
|
652
694
|
: indexerValue === undefined
|
|
653
695
|
? ""
|
|
654
|
-
: `.catchall(${typeToZod(program, indexerValue)})`;
|
|
696
|
+
: `.catchall(${withDeclarationSite(model, () => typeToZod(program, indexerValue))})`;
|
|
655
697
|
/**
|
|
656
698
|
* A request model REJECTS an unrecognised field rather than stripping it.
|
|
657
699
|
*
|
|
@@ -703,7 +745,7 @@ function modelToZod(program, model) {
|
|
|
703
745
|
.filter((property) => !isNeverType(property.type))
|
|
704
746
|
.map((property) => {
|
|
705
747
|
const key = propertyKey(program, property);
|
|
706
|
-
const { value, deferred } = captureBackEdges(() => propertyToZod(program, property));
|
|
748
|
+
const { value, deferred } = captureBackEdges(() => withDeclarationSite(property, () => propertyToZod(program, property)));
|
|
707
749
|
return {
|
|
708
750
|
deferred,
|
|
709
751
|
text: deferred ? `\tget ${key}() {\n\t\treturn ${value};\n\t},` : `\t${key}: ${value},`,
|
|
@@ -1060,10 +1102,10 @@ export function typeToZodBody(program, type) {
|
|
|
1060
1102
|
if (type.name === "unknown")
|
|
1061
1103
|
return "z.unknown()";
|
|
1062
1104
|
if (type.name === "void" || type.name === "never") {
|
|
1063
|
-
return refuse(program, type, `\`${type.name}\` has no runtime representation`);
|
|
1105
|
+
return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never` PROPERTY is dropped to match the document, and `Record<never>` seals the model, so neither of those reaches here.");
|
|
1064
1106
|
}
|
|
1065
|
-
return refuse(program, type, `intrinsic "${type.name}"
|
|
1107
|
+
return refuse(program, type, `intrinsic "${type.name}"`, "This emitter has no rule for that intrinsic. Replace it with a declared scalar or model.");
|
|
1066
1108
|
default:
|
|
1067
|
-
return refuse(program, type, "no rule for this kind");
|
|
1109
|
+
return refuse(program, type, "no rule for this kind", "Replace it with a model, scalar, enum or union, which are the kinds this emitter walks.");
|
|
1068
1110
|
}
|
|
1069
1111
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|