typespec-http-zod 0.26.0 → 0.27.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.d.ts +21 -0
- package/dist/src/api.js +107 -16
- package/dist/src/constraints.d.ts +46 -0
- package/dist/src/constraints.js +233 -10
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/lib.d.ts +32 -3
- package/dist/src/lib.js +53 -0
- package/dist/src/linter.d.ts +15 -0
- package/dist/src/linter.js +23 -0
- package/dist/src/registry.d.ts +1 -1
- package/dist/src/registry.js +135 -36
- package/dist/src/rules/redos-prone-pattern.rule.d.ts +27 -0
- package/dist/src/rules/redos-prone-pattern.rule.js +47 -0
- package/dist/src/zod.d.ts +10 -17
- package/dist/src/zod.js +50 -13
- package/package.json +6 -3
package/dist/src/zod.js
CHANGED
|
@@ -11,7 +11,7 @@ import { reportDiagnostic } from "./lib.js";
|
|
|
11
11
|
* Zod, rejects everything, and unreachable in practice because the error diagnostic has already
|
|
12
12
|
* failed the compile that would have written it.
|
|
13
13
|
*/
|
|
14
|
-
const UNREPRESENTABLE = "z.never()";
|
|
14
|
+
export const UNREPRESENTABLE = "z.never()";
|
|
15
15
|
/**
|
|
16
16
|
* **Installed by the registry, which is the only place that knows what a declared type is CALLED.**
|
|
17
17
|
*
|
|
@@ -339,16 +339,36 @@ const SCALARS = {
|
|
|
339
339
|
string: "z.string()",
|
|
340
340
|
boolean: "z.boolean()",
|
|
341
341
|
bytes: "z.string()",
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
342
|
+
/**
|
|
343
|
+
* **A declared WIDTH is a claim about the value too, and ten of them used to share one check.**
|
|
344
|
+
*
|
|
345
|
+
* Every integer scalar emitted `z.number().int()`, which is "an integer within the safe range" and
|
|
346
|
+
* nothing more. Measured at the wire on `workerd`, against a document publishing the widths below:
|
|
347
|
+
* a `uint8` accepted `-5` and `100000`, an `int32` accepted `3000000000` and `-3000000000`.
|
|
348
|
+
*
|
|
349
|
+
* The differential oracle cannot see it. JSON Schema 2020-12 treats `format` as an annotation, so
|
|
350
|
+
* Ajv on the published document answers VALID for all of those and agrees with the server while
|
|
351
|
+
* both are wrong. Ajv says so out loud: `unknown format "uint8" ignored`.
|
|
352
|
+
*
|
|
353
|
+
* Zod 4 has the named formats for the two widths that have them, and they emit better JSON Schema
|
|
354
|
+
* than a bare bound would. The rest take explicit bounds.
|
|
355
|
+
*
|
|
356
|
+
* **`int64` and `uint64` keep the safe-integer ceiling, and that is a limit rather than a
|
|
357
|
+
* choice.** `JSON.parse` has already lost a value above 2^53 before any validator runs, so the
|
|
358
|
+
* alternatives are to refuse it or to accept a number that is not the one the caller sent.
|
|
359
|
+
* Refusing is the honest one. `z.int64()` exists but is a `bigint`, which a JSON body cannot
|
|
360
|
+
* carry. What changes here is only that `uint64` stops accepting negatives.
|
|
361
|
+
*/
|
|
362
|
+
int8: "z.int().min(-128).max(127)",
|
|
363
|
+
int16: "z.int().min(-32768).max(32767)",
|
|
364
|
+
int32: "z.int32()",
|
|
365
|
+
int64: "z.int()",
|
|
366
|
+
safeint: "z.int()",
|
|
367
|
+
uint8: "z.int().min(0).max(255)",
|
|
368
|
+
uint16: "z.int().min(0).max(65535)",
|
|
369
|
+
uint32: "z.uint32()",
|
|
370
|
+
uint64: "z.int().min(0)",
|
|
371
|
+
integer: "z.int()",
|
|
352
372
|
float: "z.number()",
|
|
353
373
|
float32: "z.number()",
|
|
354
374
|
float64: "z.number()",
|
|
@@ -859,8 +879,25 @@ function modelToZod(program, model) {
|
|
|
859
879
|
* is a number - the disagreement is with the wire.
|
|
860
880
|
*
|
|
861
881
|
* Reading the TYPE answers the question the emitted text was standing in for. `SCALARS` remains the
|
|
862
|
-
* single list of what becomes a number
|
|
882
|
+
* single list of what becomes a number.
|
|
883
|
+
*
|
|
884
|
+
* **It still inspects the emitted expression to decide WHICH kind, and that did drift once more.**
|
|
885
|
+
* The test was `startsWith("z.number()")`, and giving each integer width its declared bounds spelled
|
|
886
|
+
* nine of them `z.int(`, `z.int32(` and `z.uint32(` instead. Every one silently lost its wire
|
|
887
|
+
* decoder: 22 arms went red at once, which is the only reason it was not shipped. The prefixes live
|
|
888
|
+
* in one named constant now.
|
|
889
|
+
*
|
|
890
|
+
* **That claim named a `scalars.test.ts` which did not exist, and nothing graded the list.**
|
|
891
|
+
* Measured: dropping `z.uint32(` from it, and separately `z.int(`, left all 468 arms in this
|
|
892
|
+
* package green, because no fixture had ever declared an integer width as a PARAMETER - the only
|
|
893
|
+
* place the decoder is attached. `test/widths/` now declares all ten as query parameters and sends
|
|
894
|
+
* each as text, so a spelling this list forgets fails here rather than at a consumer.
|
|
895
|
+
*/
|
|
896
|
+
/**
|
|
897
|
+
* Every emitted expression that denotes a JSON number. Named once, because `SCALARS` and this list
|
|
898
|
+
* have to agree and nothing about the two being in the same file makes them.
|
|
863
899
|
*/
|
|
900
|
+
const NUMERIC_EXPRESSIONS = ["z.number()", "z.int(", "z.int32(", "z.uint32("];
|
|
864
901
|
export function wireKindOf(program, type) {
|
|
865
902
|
if (type.kind === "Scalar") {
|
|
866
903
|
const encoded = encodedTypeOf(program, type);
|
|
@@ -870,7 +907,7 @@ export function wireKindOf(program, type) {
|
|
|
870
907
|
const mapped = SCALARS[current.name];
|
|
871
908
|
if (mapped === undefined)
|
|
872
909
|
continue;
|
|
873
|
-
if (mapped.startsWith(
|
|
910
|
+
if (NUMERIC_EXPRESSIONS.some((prefix) => mapped.startsWith(prefix)))
|
|
874
911
|
return "number";
|
|
875
912
|
if (mapped === "z.boolean()")
|
|
876
913
|
return "boolean";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"access": "public",
|
|
43
43
|
"provenance": true
|
|
44
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"redos-detector": "^6.1.4"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@types/node": "^26.5.1",
|
|
47
50
|
"@typespec/compiler": "1.16.0",
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
"typespec-http-zod": "link:.",
|
|
63
66
|
"vitest": "^5.0.0",
|
|
64
67
|
"yaml": "^2.9.1",
|
|
65
|
-
"zod": "^4.5
|
|
68
|
+
"zod": "^4.6.5"
|
|
66
69
|
},
|
|
67
70
|
"peerDependencies": {
|
|
68
71
|
"@typespec/compiler": "^1.15.0",
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
},
|
|
86
89
|
"tspMain": "lib/main.tsp",
|
|
87
90
|
"scripts": {
|
|
88
|
-
"build": "tsc -p tsconfig.build.json",
|
|
91
|
+
"build": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
|
|
89
92
|
"test": "tsc -p tsconfig.build.json && vitest run",
|
|
90
93
|
"typecheck": "tsc -p tsconfig.json",
|
|
91
94
|
"lint": "oxlint src/ test/",
|