@lankajs/typebox 1.0.1 → 2.0.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 +18 -8
- package/dist/index.d.ts +14 -9
- package/dist/index.js +22 -15
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/skills/lanka-typebox/SKILL.md +8 -8
- package/skills/lanka-typebox/reference.md +24 -17
package/README.md
CHANGED
|
@@ -18,27 +18,37 @@ A library in the same box. The app imports and calls it; core does not know it e
|
|
|
18
18
|
## Why a bridge rather than a name
|
|
19
19
|
|
|
20
20
|
TypeBox publishes no `~standard` at all — not asynchronously like yup, not at all —
|
|
21
|
-
so core's port cannot be handed a TypeBox schema. The bridge reads
|
|
22
|
-
`
|
|
23
|
-
|
|
21
|
+
so core's port cannot be handed a TypeBox schema. The bridge reads `Compile` and
|
|
22
|
+
`typebox/value` and produces the same two lists every package in the family produces.
|
|
23
|
+
|
|
24
|
+
## TypeBox 1.x, and where 0.34 went
|
|
25
|
+
|
|
26
|
+
TypeBox 1.x is a different package — `typebox`, not `@sinclair/typebox` — with a
|
|
27
|
+
different API, so this package's 2.x binds it and its 1.x line binds 0.34. That line
|
|
28
|
+
depends on lanka 2: an application on 0.34 stays on `@lankajs/typebox@1`, and moving
|
|
29
|
+
it to a later lanka means moving to TypeBox 1.x.
|
|
30
|
+
|
|
31
|
+
A TypeBox 1.x schema is recognised by its `~kind`, the mark every builder sets. Not by
|
|
32
|
+
`IsSchema`: that answers true for any object, since any object is a JSON Schema, and a
|
|
33
|
+
schema from another library would then validate as "accept everything".
|
|
24
34
|
|
|
25
35
|
## Why the compiled checker is cached, and why that is not premature
|
|
26
36
|
|
|
27
|
-
`
|
|
37
|
+
`Compile(schema)` turns a schema into a function, and the function is the
|
|
28
38
|
fastest validator in JavaScript. Compilation itself is not fast. Compiling on every
|
|
29
39
|
call would make this the SLOWEST package in the family while advertising the
|
|
30
40
|
opposite — the exact shape of a claim that measures well in a microbenchmark and
|
|
31
41
|
loses in an application.
|
|
32
42
|
|
|
33
43
|
So a `WeakMap` keyed by the schema object holds the compiled checker and whether the
|
|
34
|
-
schema contains a
|
|
44
|
+
schema contains a codec. Weak because the key is the consumer's schema: a strong
|
|
35
45
|
map here would keep every schema a screen ever built alive for the life of the tab.
|
|
36
46
|
|
|
37
47
|
## Why `Decode` is not simply always called
|
|
38
48
|
|
|
39
|
-
`
|
|
40
|
-
check would validate every body twice. `
|
|
41
|
-
cached beside the checker, so a schema without
|
|
49
|
+
`Decode` applies codecs AND re-checks, so calling it after the compiled
|
|
50
|
+
check would validate every body twice. `HasCodec` is asked once per schema and
|
|
51
|
+
cached beside the checker, so a schema without codecs — which is most of them —
|
|
42
52
|
pays for one pass.
|
|
43
53
|
|
|
44
54
|
---
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TSchema,
|
|
1
|
+
import { TSchema, StaticDecode } from 'typebox';
|
|
2
2
|
import { TLankaValidationResult } from 'lanka/validation';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -12,9 +12,9 @@ import { TLankaValidationResult } from 'lanka/validation';
|
|
|
12
12
|
*/
|
|
13
13
|
interface ILankaTypeBoxValidator {
|
|
14
14
|
/** Validates and returns the parsed value, or throws. */
|
|
15
|
-
validate<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown, context: string):
|
|
15
|
+
validate<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown, context: string): StaticDecode<TSchemaType>;
|
|
16
16
|
/** Validates and returns an outcome, throwing nothing. */
|
|
17
|
-
validateSafe<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown): TLankaValidationResult<
|
|
17
|
+
validateSafe<TSchemaType extends TSchema>(schema: TSchemaType, data: unknown): TLankaValidationResult<StaticDecode<TSchemaType>>;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* The validator for TypeBox schemas.
|
|
@@ -27,10 +27,10 @@ interface ILankaTypeBoxValidator {
|
|
|
27
27
|
*
|
|
28
28
|
* ## What it costs, and why the cache is not premature
|
|
29
29
|
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* `Compile(schema)` produces the fastest validator in JavaScript, and compiling
|
|
31
|
+
* is the slow part. Compiled per call, this would be the slowest package in the
|
|
32
|
+
* family while claiming to be the fastest. The checker is cached per schema
|
|
33
|
+
* instead — see `compiledTypeBoxSchema`.
|
|
34
34
|
*
|
|
35
35
|
* A schema built inside a component body is a new object on every render and
|
|
36
36
|
* therefore a new cache key. Declare schemas at module level, which is where they
|
|
@@ -38,7 +38,12 @@ interface ILankaTypeBoxValidator {
|
|
|
38
38
|
*/
|
|
39
39
|
declare const lankaTypeBoxValidator: ILankaTypeBoxValidator;
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
type
|
|
41
|
+
/**
|
|
42
|
+
* The type a schema infers: what `validate` returns.
|
|
43
|
+
*
|
|
44
|
+
* `StaticDecode` rather than `Static`, which in TypeBox 1.x is the ENCODED
|
|
45
|
+
* side — the wire shape a codec reads, not the value it produces.
|
|
46
|
+
*/
|
|
47
|
+
type TLankaInferred<TSchemaType extends TSchema> = StaticDecode<TSchemaType>;
|
|
43
48
|
|
|
44
49
|
export { type TLankaInferred, lankaTypeBoxValidator };
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
// src/lanka-type-box-validator/lankaTypeBoxValidator.ts
|
|
2
|
-
import {
|
|
3
|
-
import { Value } from "@sinclair/typebox/value";
|
|
2
|
+
import { Clone, DecodeUnsafe } from "typebox/value";
|
|
4
3
|
import { LankaValidationError } from "lanka/validation";
|
|
5
4
|
import { lankaForeignSchemaMessage, lankaValueOrThrow } from "lanka/internal";
|
|
6
5
|
|
|
7
6
|
// src/_internal/compiled-type-box-schema/compiledTypeBoxSchema.ts
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { Compile } from "typebox/compile";
|
|
8
|
+
import { HasCodec } from "typebox/value";
|
|
10
9
|
var compiled = /* @__PURE__ */ new WeakMap();
|
|
11
10
|
var compiledTypeBoxSchema = (schema) => {
|
|
12
11
|
const known = compiled.get(schema);
|
|
13
12
|
if (known) return known;
|
|
14
13
|
const fresh = {
|
|
15
|
-
check:
|
|
16
|
-
|
|
14
|
+
check: Compile(schema),
|
|
15
|
+
codecs: HasCodec(schema)
|
|
17
16
|
};
|
|
18
17
|
compiled.set(schema, fresh);
|
|
19
18
|
return fresh;
|
|
@@ -38,38 +37,46 @@ var lankaTypeBoxValidator = Object.freeze({
|
|
|
38
37
|
}
|
|
39
38
|
});
|
|
40
39
|
function runCompiled(schema, data) {
|
|
41
|
-
if (!
|
|
42
|
-
const { check,
|
|
43
|
-
if (!check.Check(data)) return describeFailure(
|
|
44
|
-
if (!
|
|
40
|
+
if (!isTypeBoxSchema(schema)) throw notATypeBoxSchema(schema);
|
|
41
|
+
const { check, codecs } = compiledTypeBoxSchema(schema);
|
|
42
|
+
if (!check.Check(data)) return describeFailure(check.Errors(data));
|
|
43
|
+
if (!codecs) return { success: true, data };
|
|
45
44
|
return decode(schema, data);
|
|
46
45
|
}
|
|
46
|
+
function isTypeBoxSchema(schema) {
|
|
47
|
+
if (typeof schema !== "object" || schema === null) return false;
|
|
48
|
+
return typeof schema["~kind"] === "string";
|
|
49
|
+
}
|
|
47
50
|
function notATypeBoxSchema(schema) {
|
|
48
51
|
return new LankaValidationError(
|
|
49
52
|
lankaForeignSchemaMessage(schema, {
|
|
50
|
-
lead: "This is not a TypeBox schema: it carries no
|
|
53
|
+
lead: "This is not a TypeBox schema: it carries no `~kind`."
|
|
51
54
|
}),
|
|
52
55
|
[]
|
|
53
56
|
);
|
|
54
57
|
}
|
|
55
58
|
function decode(schema, data) {
|
|
56
59
|
try {
|
|
57
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
data: DecodeUnsafe({}, schema, Clone(data))
|
|
63
|
+
};
|
|
58
64
|
} catch (error) {
|
|
59
65
|
const message = decodeFailureMessage(error);
|
|
60
66
|
return { success: false, errors: [message], fields: [{ path: [], message }] };
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
69
|
function decodeFailureMessage(error) {
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
if (error instanceof Error) return error.message;
|
|
71
|
+
if (typeof error === "string") return error;
|
|
72
|
+
return "The schema's decode function refused the value without saying why.";
|
|
66
73
|
}
|
|
67
74
|
function describeFailure(errors) {
|
|
68
75
|
const fields = errors.map(toFieldError);
|
|
69
76
|
return { success: false, errors: fields.map(describeField), fields };
|
|
70
77
|
}
|
|
71
78
|
function toFieldError(error) {
|
|
72
|
-
return { path: typeBoxPointerSegments(error.
|
|
79
|
+
return { path: typeBoxPointerSegments(error.instancePath), message: error.message };
|
|
73
80
|
}
|
|
74
81
|
function describeField(field) {
|
|
75
82
|
const path = field.path.join(".");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lanka-type-box-validator/lankaTypeBoxValidator.ts","../src/_internal/compiled-type-box-schema/compiledTypeBoxSchema.ts","../src/_utils/type-box-pointer-segments/typeBoxPointerSegments.ts"],"sourcesContent":["import { KindGuard } from \"@sinclair/typebox\";\nimport { Value } from \"@sinclair/typebox/value\";\nimport { LankaValidationError } from \"lanka/validation\";\nimport { lankaForeignSchemaMessage, lankaValueOrThrow } from \"lanka/internal\";\nimport type { Static, TSchema } from \"@sinclair/typebox\";\nimport type { ValueError } from \"@sinclair/typebox/value\";\nimport type { TLankaValidationResult } from \"lanka/validation\";\nimport type { ILankaFieldError } from \"lanka/errors\";\nimport { compiledTypeBoxSchema } from \"../_internal/compiled-type-box-schema/compiledTypeBoxSchema\";\nimport { typeBoxPointerSegments } from \"../_utils/type-box-pointer-segments/typeBoxPointerSegments\";\n\n/**\n * The validator's shape, typed by a TypeBox schema rather than by core's port.\n *\n * `ILankaValidator` takes a `TLankaSchema` — a Standard Schema — and TypeBox\n * publishes none, so a validator declared as the port would reject every schema a\n * consumer of this package has. The SHAPE is the port's, member for member, and\n * `check:family` holds the barrel to the family's surface; what differs is the\n * one type the library makes impossible to share.\n */\nexport interface ILankaTypeBoxValidator {\n\t/** Validates and returns the parsed value, or throws. */\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): Static<TSchemaType>;\n\t/** Validates and returns an outcome, throwing nothing. */\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<Static<TSchemaType>>;\n}\n\n/**\n * The validator for TypeBox schemas.\n *\n * ## Why this one is a bridge\n *\n * TypeBox publishes no `~standard` at all — not asynchronously as yup does, not\n * at all — so core's port cannot be handed a TypeBox schema and the package is\n * what makes the library usable here.\n *\n * ## What it costs, and why the cache is not premature\n *\n * `TypeCompiler.Compile(schema)` produces the fastest validator in JavaScript,\n * and compiling is the slow part. Compiled per call, this would be the slowest\n * package in the family while claiming to be the fastest. The checker is cached\n * per schema instead — see `compiledTypeBoxSchema`.\n *\n * A schema built inside a component body is a new object on every render and\n * therefore a new cache key. Declare schemas at module level, which is where they\n * belong for every other reason too.\n */\nexport const lankaTypeBoxValidator: ILankaTypeBoxValidator = Object.freeze<ILankaTypeBoxValidator>({\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): Static<TSchemaType> {\n\t\treturn lankaValueOrThrow(runCompiled(schema, data), context);\n\t},\n\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<Static<TSchemaType>> {\n\t\treturn runCompiled(schema, data);\n\t},\n});\n\n/**\n * One pass through the compiled checker, and a second only if the schema asks.\n *\n * `Value.Decode` applies transforms AND re-checks, so calling it unconditionally\n * would validate every body twice. Whether the schema transforms at all is\n * answered once per schema and cached beside the checker.\n */\nfunction runCompiled<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<Static<TSchemaType>> {\n\tif (!KindGuard.IsSchema(schema)) throw notATypeBoxSchema(schema);\n\n\tconst { check, transforms } = compiledTypeBoxSchema(schema);\n\n\tif (!check.Check(data)) return describeFailure([...check.Errors(data)]);\n\tif (!transforms) return { success: true, data: data as Static<TSchemaType> };\n\n\treturn decode(schema, data);\n}\n\n/**\n * The refusal for a schema from another library.\n *\n * An application whose schemas come from two libraries eventually hands one to\n * the wrong validator. Unguarded, `TypeCompiler.Compile` was handed a zod schema\n * and threw `TypeCompilerTypeGuardError: Preflight validation check failed` —\n * accurate, and useless to anyone who has not read TypeBox's source. It escaped\n * `validateSafe`, which promises to throw nothing, as a raw library error.\n *\n * `KindGuard.IsSchema` is TypeBox's own answer to the question, so the guard\n * cannot drift from what `Compile` will accept.\n *\n * It throws from `validateSafe` too, deliberately: a refused VALUE is an outcome\n * a form renders, while a schema this package cannot read is a wiring mistake,\n * and putting it in `errors` would show a programmer's error to a user beside an\n * input.\n */\nfunction notATypeBoxSchema(schema: unknown): LankaValidationError {\n\treturn new LankaValidationError(\n\t\tlankaForeignSchemaMessage(schema, {\n\t\t\tlead: \"This is not a TypeBox schema: it carries no `Kind`.\",\n\t\t}),\n\t\t[],\n\t);\n}\n\n/**\n * The transform pass, for a schema that has one.\n *\n * A decode function is consumer code and may throw — a date that does not parse,\n * an enum with no case for the value. That is a refusal of the body, not a crash\n * of the validator, so it comes back as one: `validateSafe` promises to throw\n * nothing.\n */\nfunction decode<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<Static<TSchemaType>> {\n\ttry {\n\t\treturn { success: true, data: Value.Decode(schema, data) };\n\t} catch (error) {\n\t\tconst message = decodeFailureMessage(error);\n\n\t\treturn { success: false, errors: [message], fields: [{ path: [], message }] };\n\t}\n}\n\n/**\n * What a failed decode should SAY.\n *\n * TypeBox wraps whatever the decode function threw in a `TransformDecodeError`\n * and copies across a message only when the thrown value was an `Error` —\n * anything else becomes the literal string \"Unknown error\", which tells a\n * consumer nothing about their own code. The original is kept on `.error`, so\n * that is what is read first.\n */\nfunction decodeFailureMessage(error: unknown): string {\n\tconst original = (error as { error?: unknown }).error ?? error;\n\n\treturn original instanceof Error ? original.message : String(original);\n}\n\n/** A TypeBox failure as the two lists the port promises: one for a banner, one for a form. */\nfunction describeFailure<TOutput>(errors: ValueError[]): TLankaValidationResult<TOutput> {\n\tconst fields = errors.map(toFieldError);\n\n\treturn { success: false, errors: fields.map(describeField), fields };\n}\n\nfunction toFieldError(error: ValueError): ILankaFieldError {\n\treturn { path: typeBoxPointerSegments(error.path), message: error.message };\n}\n\n/**\n * A field path plus its message, for a banner.\n *\n * Dots rather than TypeBox's pointer spelling, because this string is read beside\n * the other five packages' and the family reads one way.\n */\nfunction describeField(field: ILankaFieldError): string {\n\tconst path = field.path.join(\".\");\n\n\treturn path ? `${path}: ${field.message}` : field.message;\n}\n","import { TypeCompiler } from \"@sinclair/typebox/compiler\";\nimport { HasTransform } from \"@sinclair/typebox/value\";\nimport type { TSchema } from \"@sinclair/typebox\";\nimport type { TypeCheck } from \"@sinclair/typebox/compiler\";\n\n/** What is known about one schema once, and re-read on every call after that. */\nexport interface ICompiledTypeBoxSchema {\n\t/** The compiled checker: a function TypeBox generated for this schema. */\n\tcheck: TypeCheck<TSchema>;\n\t/**\n\t * Whether anything in the schema transforms.\n\t *\n\t * Asked once because the answer cannot change — a schema is a value — and\n\t * because it decides whether a second pass is needed at all. Most schemas do\n\t * not transform, and those must not pay for `Value.Decode`'s re-check.\n\t */\n\ttransforms: boolean;\n}\n\n/**\n * The compiled checker for a schema, compiled at most once.\n *\n * ## Why this exists\n *\n * `TypeCompiler.Compile` turns a schema into a generated function, and that\n * function is the fastest validator in JavaScript. Compiling is not fast.\n * Compiling per call would make this the SLOWEST package in the family while the\n * README advertised the opposite — the exact shape of a claim that wins a\n * microbenchmark and loses in an application.\n *\n * ## Why a WeakMap\n *\n * The key is the CONSUMER'S schema object. A strong map would keep every schema\n * any screen ever built alive for the life of the tab, which is a leak the\n * consumer cannot see, cannot measure and cannot clear. Weak, the entry goes when\n * the schema does.\n *\n * A schema rebuilt on every render defeats the cache — it is a new key each\n * time — and that is a fact worth knowing rather than a case to work around:\n * declare schemas at module level, which is where they belong anyway.\n */\nconst compiled = new WeakMap<TSchema, ICompiledTypeBoxSchema>();\n\nexport const compiledTypeBoxSchema = (schema: TSchema): ICompiledTypeBoxSchema => {\n\tconst known = compiled.get(schema);\n\tif (known) return known;\n\n\tconst fresh: ICompiledTypeBoxSchema = {\n\t\tcheck: TypeCompiler.Compile(schema),\n\t\ttransforms: HasTransform(schema, []),\n\t};\n\n\tcompiled.set(schema, fresh);\n\n\treturn fresh;\n};\n","/**\n * A TypeBox error pointer as segments: `/tags/0/id` → `[\"tags\", 0, \"id\"]`.\n *\n * TypeBox addresses a field with a JSON Pointer (RFC 6901). `ILankaFieldError.path`\n * is segments, on purpose: the two form libraries the shape was designed against\n * spell the same address differently, and neither can be parsed back out of a\n * joined string safely.\n *\n * The pointer's two escapes are not decoration. `~1` is a literal `/` in a key\n * and `~0` a literal `~`; unescaped, a key containing a slash would split into\n * two segments addressing nothing. **In this order** — `~1` first, then `~0` —\n * because the reverse turns `~01` into `/` instead of the literal `~1` it is.\n *\n * A segment of digits becomes a NUMBER. A form distinguishes the second element\n * of a list from a key spelled `\"1\"`, and JSON Pointer does not carry the\n * difference itself — it is recovered from the shape of the segment, which is\n * what every JSON Pointer implementation does.\n *\n * The root pointer is the empty string, and an empty array is what the port's\n * consumers already read as \"the value as a whole\".\n */\nexport const typeBoxPointerSegments = (pointer: string | undefined): (string | number)[] => {\n\tif (!pointer) return [];\n\n\treturn pointer\n\t\t.split(\"/\")\n\t\t.slice(1)\n\t\t.map((segment) => {\n\t\t\tconst key = segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n\n\t\t\treturn /^\\d+$/.test(key) ? Number(key) : key;\n\t\t});\n};\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,aAAa;AACtB,SAAS,4BAA4B;AACrC,SAAS,2BAA2B,yBAAyB;;;ACH7D,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAwC7B,IAAM,WAAW,oBAAI,QAAyC;AAEvD,IAAM,wBAAwB,CAAC,WAA4C;AACjF,QAAM,QAAQ,SAAS,IAAI,MAAM;AACjC,MAAI,MAAO,QAAO;AAElB,QAAM,QAAgC;AAAA,IACrC,OAAO,aAAa,QAAQ,MAAM;AAAA,IAClC,YAAY,aAAa,QAAQ,CAAC,CAAC;AAAA,EACpC;AAEA,WAAS,IAAI,QAAQ,KAAK;AAE1B,SAAO;AACR;;;AClCO,IAAM,yBAAyB,CAAC,YAAqD;AAC3F,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,SAAO,QACL,MAAM,GAAG,EACT,MAAM,CAAC,EACP,IAAI,CAAC,YAAY;AACjB,UAAM,MAAM,QAAQ,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AAE1D,WAAO,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;AAAA,EAC1C,CAAC;AACH;;;AFsBO,IAAM,wBAAgD,OAAO,OAA+B;AAAA,EAClG,SACC,QACA,MACA,SACsB;AACtB,WAAO,kBAAkB,YAAY,QAAQ,IAAI,GAAG,OAAO;AAAA,EAC5D;AAAA,EAEA,aACC,QACA,MAC8C;AAC9C,WAAO,YAAY,QAAQ,IAAI;AAAA,EAChC;AACD,CAAC;AASD,SAAS,YACR,QACA,MAC8C;AAC9C,MAAI,CAAC,UAAU,SAAS,MAAM,EAAG,OAAM,kBAAkB,MAAM;AAE/D,QAAM,EAAE,OAAO,WAAW,IAAI,sBAAsB,MAAM;AAE1D,MAAI,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO,gBAAgB,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,CAAC;AACtE,MAAI,CAAC,WAAY,QAAO,EAAE,SAAS,MAAM,KAAkC;AAE3E,SAAO,OAAO,QAAQ,IAAI;AAC3B;AAmBA,SAAS,kBAAkB,QAAuC;AACjE,SAAO,IAAI;AAAA,IACV,0BAA0B,QAAQ;AAAA,MACjC,MAAM;AAAA,IACP,CAAC;AAAA,IACD,CAAC;AAAA,EACF;AACD;AAUA,SAAS,OACR,QACA,MAC8C;AAC9C,MAAI;AACH,WAAO,EAAE,SAAS,MAAM,MAAM,MAAM,OAAO,QAAQ,IAAI,EAAE;AAAA,EAC1D,SAAS,OAAO;AACf,UAAM,UAAU,qBAAqB,KAAK;AAE1C,WAAO,EAAE,SAAS,OAAO,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,EAC7E;AACD;AAWA,SAAS,qBAAqB,OAAwB;AACrD,QAAM,WAAY,MAA8B,SAAS;AAEzD,SAAO,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ;AACtE;AAGA,SAAS,gBAAyB,QAAuD;AACxF,QAAM,SAAS,OAAO,IAAI,YAAY;AAEtC,SAAO,EAAE,SAAS,OAAO,QAAQ,OAAO,IAAI,aAAa,GAAG,OAAO;AACpE;AAEA,SAAS,aAAa,OAAqC;AAC1D,SAAO,EAAE,MAAM,uBAAuB,MAAM,IAAI,GAAG,SAAS,MAAM,QAAQ;AAC3E;AAQA,SAAS,cAAc,OAAiC;AACvD,QAAM,OAAO,MAAM,KAAK,KAAK,GAAG;AAEhC,SAAO,OAAO,GAAG,IAAI,KAAK,MAAM,OAAO,KAAK,MAAM;AACnD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/lanka-type-box-validator/lankaTypeBoxValidator.ts","../src/_internal/compiled-type-box-schema/compiledTypeBoxSchema.ts","../src/_utils/type-box-pointer-segments/typeBoxPointerSegments.ts"],"sourcesContent":["import { Clone, DecodeUnsafe } from \"typebox/value\";\nimport { LankaValidationError } from \"lanka/validation\";\nimport { lankaForeignSchemaMessage, lankaValueOrThrow } from \"lanka/internal\";\nimport type { StaticDecode, TSchema } from \"typebox\";\nimport type { TLocalizedValidationError } from \"typebox/error\";\nimport type { TLankaValidationResult } from \"lanka/validation\";\nimport type { ILankaFieldError } from \"lanka/errors\";\nimport { compiledTypeBoxSchema } from \"../_internal/compiled-type-box-schema/compiledTypeBoxSchema\";\nimport { typeBoxPointerSegments } from \"../_utils/type-box-pointer-segments/typeBoxPointerSegments\";\n\n/**\n * The validator's shape, typed by a TypeBox schema rather than by core's port.\n *\n * `ILankaValidator` takes a `TLankaSchema` — a Standard Schema — and TypeBox\n * publishes none, so a validator declared as the port would reject every schema a\n * consumer of this package has. The SHAPE is the port's, member for member, and\n * `check:family` holds the barrel to the family's surface; what differs is the\n * one type the library makes impossible to share.\n */\nexport interface ILankaTypeBoxValidator {\n\t/** Validates and returns the parsed value, or throws. */\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): StaticDecode<TSchemaType>;\n\t/** Validates and returns an outcome, throwing nothing. */\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<StaticDecode<TSchemaType>>;\n}\n\n/**\n * The validator for TypeBox schemas.\n *\n * ## Why this one is a bridge\n *\n * TypeBox publishes no `~standard` at all — not asynchronously as yup does, not\n * at all — so core's port cannot be handed a TypeBox schema and the package is\n * what makes the library usable here.\n *\n * ## What it costs, and why the cache is not premature\n *\n * `Compile(schema)` produces the fastest validator in JavaScript, and compiling\n * is the slow part. Compiled per call, this would be the slowest package in the\n * family while claiming to be the fastest. The checker is cached per schema\n * instead — see `compiledTypeBoxSchema`.\n *\n * A schema built inside a component body is a new object on every render and\n * therefore a new cache key. Declare schemas at module level, which is where they\n * belong for every other reason too.\n */\nexport const lankaTypeBoxValidator: ILankaTypeBoxValidator = Object.freeze<ILankaTypeBoxValidator>({\n\tvalidate<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t\tcontext: string,\n\t): StaticDecode<TSchemaType> {\n\t\treturn lankaValueOrThrow(runCompiled(schema, data), context);\n\t},\n\n\tvalidateSafe<TSchemaType extends TSchema>(\n\t\tschema: TSchemaType,\n\t\tdata: unknown,\n\t): TLankaValidationResult<StaticDecode<TSchemaType>> {\n\t\treturn runCompiled(schema, data);\n\t},\n});\n\n/**\n * One pass through the compiled checker, and a second only if the schema asks.\n *\n * Whether the schema has a codec at all is answered once per schema and cached\n * beside the checker, so the common schema — no codec — costs one generated\n * function call and nothing else.\n */\nfunction runCompiled<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<StaticDecode<TSchemaType>> {\n\tif (!isTypeBoxSchema(schema)) throw notATypeBoxSchema(schema);\n\n\tconst { check, codecs } = compiledTypeBoxSchema(schema);\n\n\tif (!check.Check(data)) return describeFailure(check.Errors(data));\n\tif (!codecs) return { success: true, data: data as StaticDecode<TSchemaType> };\n\n\treturn decode(schema, data);\n}\n\n/**\n * Whether a value was built by TypeBox 1.x.\n *\n * Not `IsSchema`: that answers true for ANY object, because any object is a JSON\n * Schema — and `Compile` agrees, so a zod schema handed here would compile to a\n * checker that accepts everything. `~kind` is the mark every TypeBox builder\n * sets, as a non-enumerable own property, and it is what this reads.\n */\nfunction isTypeBoxSchema(schema: unknown): schema is TSchema {\n\tif (typeof schema !== \"object\" || schema === null) return false;\n\n\treturn typeof (schema as { \"~kind\"?: unknown })[\"~kind\"] === \"string\";\n}\n\n/**\n * The refusal for a schema from another library.\n *\n * An application whose schemas come from two libraries eventually hands one to\n * the wrong validator. Unguarded, TypeBox 0.34's compiler threw\n * `TypeCompilerTypeGuardError: Preflight validation check failed` — accurate, and\n * useless to anyone who has not read TypeBox's source — and TypeBox 1.x would say\n * nothing at all. Either way, a raw library outcome escaped `validateSafe`.\n *\n * It throws from `validateSafe` too, deliberately: a refused VALUE is an outcome\n * a form renders, while a schema this package cannot read is a wiring mistake,\n * and putting it in `errors` would show a programmer's error to a user beside an\n * input.\n */\nfunction notATypeBoxSchema(schema: unknown): LankaValidationError {\n\treturn new LankaValidationError(\n\t\tlankaForeignSchemaMessage(schema, {\n\t\t\tlead: \"This is not a TypeBox schema: it carries no `~kind`.\",\n\t\t}),\n\t\t[],\n\t);\n}\n\n/**\n * The codec pass, for a schema that has one.\n *\n * `DecodeUnsafe` rather than `Decode`, because `Decode` is a pipeline — clone,\n * default, convert, CLEAN, check, decode — and two of those steps would make this\n * path disagree with the one without a codec: the check was already done, and\n * cleaning drops every property the schema did not name, which the path without\n * a codec keeps. `DecodeUnsafe` runs the decode functions and nothing else — and\n * writes their results into the object it is handed, so it is handed a copy: the\n * body is the caller's.\n *\n * A decode function is consumer code and may throw — a date that does not parse,\n * an enum with no case for the value. That is a refusal of the body, not a crash\n * of the validator, so it comes back as one: `validateSafe` promises to throw\n * nothing.\n */\nfunction decode<TSchemaType extends TSchema>(\n\tschema: TSchemaType,\n\tdata: unknown,\n): TLankaValidationResult<StaticDecode<TSchemaType>> {\n\ttry {\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: DecodeUnsafe({}, schema, Clone(data)) as StaticDecode<TSchemaType>,\n\t\t};\n\t} catch (error) {\n\t\tconst message = decodeFailureMessage(error);\n\n\t\treturn { success: false, errors: [message], fields: [{ path: [], message }] };\n\t}\n}\n\n/**\n * What a failed decode should SAY.\n *\n * TypeBox 1.x rethrows whatever the decode function threw, unwrapped. An `Error`\n * has a message and a string is one; anything else has nothing to read, and\n * `String()` of it would put \"undefined\" or \"[object Object]\" in front of a user.\n */\nfunction decodeFailureMessage(error: unknown): string {\n\tif (error instanceof Error) return error.message;\n\tif (typeof error === \"string\") return error;\n\n\treturn \"The schema's decode function refused the value without saying why.\";\n}\n\n/** A TypeBox failure as the two lists the port promises: one for a banner, one for a form. */\nfunction describeFailure<TOutput>(\n\terrors: readonly TLocalizedValidationError[],\n): TLankaValidationResult<TOutput> {\n\tconst fields = errors.map(toFieldError);\n\n\treturn { success: false, errors: fields.map(describeField), fields };\n}\n\nfunction toFieldError(error: TLocalizedValidationError): ILankaFieldError {\n\treturn { path: typeBoxPointerSegments(error.instancePath), message: error.message };\n}\n\n/**\n * A field path plus its message, for a banner.\n *\n * Dots rather than TypeBox's pointer spelling, because this string is read beside\n * the other five packages' and the family reads one way.\n */\nfunction describeField(field: ILankaFieldError): string {\n\tconst path = field.path.join(\".\");\n\n\treturn path ? `${path}: ${field.message}` : field.message;\n}\n","import { Compile } from \"typebox/compile\";\nimport { HasCodec } from \"typebox/value\";\nimport type { TSchema } from \"typebox\";\nimport type { Validator } from \"typebox/compile\";\n\n/** What is known about one schema once, and re-read on every call after that. */\nexport interface ICompiledTypeBoxSchema {\n\t/** The compiled checker: a function TypeBox generated for this schema. */\n\tcheck: Validator;\n\t/**\n\t * Whether anything in the schema has a codec.\n\t *\n\t * Asked once because the answer cannot change — a schema is a value — and\n\t * because it decides whether a second pass is needed at all. Most schemas have\n\t * no codec, and those must not pay for a copy and a decode.\n\t */\n\tcodecs: boolean;\n}\n\n/**\n * The compiled checker for a schema, compiled at most once.\n *\n * ## Why this exists\n *\n * `Compile` turns a schema into a generated function, and that\n * function is the fastest validator in JavaScript. Compiling is not fast.\n * Compiling per call would make this the SLOWEST package in the family while the\n * README advertised the opposite — the exact shape of a claim that wins a\n * microbenchmark and loses in an application.\n *\n * ## Why a WeakMap\n *\n * The key is the CONSUMER'S schema object. A strong map would keep every schema\n * any screen ever built alive for the life of the tab, which is a leak the\n * consumer cannot see, cannot measure and cannot clear. Weak, the entry goes when\n * the schema does.\n *\n * A schema rebuilt on every render defeats the cache — it is a new key each\n * time — and that is a fact worth knowing rather than a case to work around:\n * declare schemas at module level, which is where they belong anyway.\n */\nconst compiled = new WeakMap<TSchema, ICompiledTypeBoxSchema>();\n\nexport const compiledTypeBoxSchema = (schema: TSchema): ICompiledTypeBoxSchema => {\n\tconst known = compiled.get(schema);\n\tif (known) return known;\n\n\tconst fresh: ICompiledTypeBoxSchema = {\n\t\tcheck: Compile(schema),\n\t\tcodecs: HasCodec(schema),\n\t};\n\n\tcompiled.set(schema, fresh);\n\n\treturn fresh;\n};\n","/**\n * A TypeBox error pointer as segments: `/tags/0/id` → `[\"tags\", 0, \"id\"]`.\n *\n * TypeBox addresses a field with a JSON Pointer (RFC 6901). `ILankaFieldError.path`\n * is segments, on purpose: the two form libraries the shape was designed against\n * spell the same address differently, and neither can be parsed back out of a\n * joined string safely.\n *\n * The pointer's two escapes are not decoration. `~1` is a literal `/` in a key\n * and `~0` a literal `~`; unescaped, a key containing a slash would split into\n * two segments addressing nothing. **In this order** — `~1` first, then `~0` —\n * because the reverse turns `~01` into `/` instead of the literal `~1` it is.\n *\n * A segment of digits becomes a NUMBER. A form distinguishes the second element\n * of a list from a key spelled `\"1\"`, and JSON Pointer does not carry the\n * difference itself — it is recovered from the shape of the segment, which is\n * what every JSON Pointer implementation does.\n *\n * The root pointer is the empty string, and an empty array is what the port's\n * consumers already read as \"the value as a whole\".\n */\nexport const typeBoxPointerSegments = (pointer: string | undefined): (string | number)[] => {\n\tif (!pointer) return [];\n\n\treturn pointer\n\t\t.split(\"/\")\n\t\t.slice(1)\n\t\t.map((segment) => {\n\t\t\tconst key = segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n\n\t\t\treturn /^\\d+$/.test(key) ? Number(key) : key;\n\t\t});\n};\n"],"mappings":";AAAA,SAAS,OAAO,oBAAoB;AACpC,SAAS,4BAA4B;AACrC,SAAS,2BAA2B,yBAAyB;;;ACF7D,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAwCzB,IAAM,WAAW,oBAAI,QAAyC;AAEvD,IAAM,wBAAwB,CAAC,WAA4C;AACjF,QAAM,QAAQ,SAAS,IAAI,MAAM;AACjC,MAAI,MAAO,QAAO;AAElB,QAAM,QAAgC;AAAA,IACrC,OAAO,QAAQ,MAAM;AAAA,IACrB,QAAQ,SAAS,MAAM;AAAA,EACxB;AAEA,WAAS,IAAI,QAAQ,KAAK;AAE1B,SAAO;AACR;;;AClCO,IAAM,yBAAyB,CAAC,YAAqD;AAC3F,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,SAAO,QACL,MAAM,GAAG,EACT,MAAM,CAAC,EACP,IAAI,CAAC,YAAY;AACjB,UAAM,MAAM,QAAQ,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AAE1D,WAAO,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;AAAA,EAC1C,CAAC;AACH;;;AFqBO,IAAM,wBAAgD,OAAO,OAA+B;AAAA,EAClG,SACC,QACA,MACA,SAC4B;AAC5B,WAAO,kBAAkB,YAAY,QAAQ,IAAI,GAAG,OAAO;AAAA,EAC5D;AAAA,EAEA,aACC,QACA,MACoD;AACpD,WAAO,YAAY,QAAQ,IAAI;AAAA,EAChC;AACD,CAAC;AASD,SAAS,YACR,QACA,MACoD;AACpD,MAAI,CAAC,gBAAgB,MAAM,EAAG,OAAM,kBAAkB,MAAM;AAE5D,QAAM,EAAE,OAAO,OAAO,IAAI,sBAAsB,MAAM;AAEtD,MAAI,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO,gBAAgB,MAAM,OAAO,IAAI,CAAC;AACjE,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS,MAAM,KAAwC;AAE7E,SAAO,OAAO,QAAQ,IAAI;AAC3B;AAUA,SAAS,gBAAgB,QAAoC;AAC5D,MAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO;AAE1D,SAAO,OAAQ,OAAiC,OAAO,MAAM;AAC9D;AAgBA,SAAS,kBAAkB,QAAuC;AACjE,SAAO,IAAI;AAAA,IACV,0BAA0B,QAAQ;AAAA,MACjC,MAAM;AAAA,IACP,CAAC;AAAA,IACD,CAAC;AAAA,EACF;AACD;AAkBA,SAAS,OACR,QACA,MACoD;AACpD,MAAI;AACH,WAAO;AAAA,MACN,SAAS;AAAA,MACT,MAAM,aAAa,CAAC,GAAG,QAAQ,MAAM,IAAI,CAAC;AAAA,IAC3C;AAAA,EACD,SAAS,OAAO;AACf,UAAM,UAAU,qBAAqB,KAAK;AAE1C,WAAO,EAAE,SAAS,OAAO,QAAQ,CAAC,OAAO,GAAG,QAAQ,CAAC,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,EAAE;AAAA,EAC7E;AACD;AASA,SAAS,qBAAqB,OAAwB;AACrD,MAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,SAAO;AACR;AAGA,SAAS,gBACR,QACkC;AAClC,QAAM,SAAS,OAAO,IAAI,YAAY;AAEtC,SAAO,EAAE,SAAS,OAAO,QAAQ,OAAO,IAAI,aAAa,GAAG,OAAO;AACpE;AAEA,SAAS,aAAa,OAAoD;AACzE,SAAO,EAAE,MAAM,uBAAuB,MAAM,YAAY,GAAG,SAAS,MAAM,QAAQ;AACnF;AAQA,SAAS,cAAc,OAAiC;AACvD,QAAM,OAAO,MAAM,KAAK,KAAK,GAAG;AAEhC,SAAO,OAAO,GAAG,IAAI,KAAK,MAAM,OAAO,KAAK,MAAM;AACnD;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lankajs/typebox",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "module: A bridge for the one library in the family with no Standard Schema, and a compiled checker it caches.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
"skills"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"lanka": "^2.
|
|
29
|
+
"lanka": "^2.2.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"
|
|
32
|
+
"typebox": "^1.3.34",
|
|
33
|
+
"@lankajs/tool-testing": "^2.2.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"
|
|
36
|
+
"typebox": "^1.3.34"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsup",
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
metadata:
|
|
6
6
|
author: lankajs
|
|
7
7
|
package: @lankajs/typebox
|
|
8
|
-
version: "
|
|
8
|
+
version: "2.0.0"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# @lankajs/typebox
|
|
@@ -28,7 +28,7 @@ Install this **or** another package from `modules/validators/`, never two.
|
|
|
28
28
|
## Declare schemas at MODULE level
|
|
29
29
|
|
|
30
30
|
This is the one rule that costs real performance if ignored.
|
|
31
|
-
`
|
|
31
|
+
`Compile(schema)` produces the fastest validator in JavaScript, and
|
|
32
32
|
compiling is the slow part. The package compiles each schema once and caches it
|
|
33
33
|
in a `WeakMap` keyed by the schema OBJECT.
|
|
34
34
|
|
|
@@ -80,7 +80,7 @@ as a **number**, and `~1` / `~0` escapes are decoded. A root failure has an
|
|
|
80
80
|
## Mapping a wire format
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
|
-
const todoFromApi = Type.
|
|
83
|
+
const todoFromApi = Type.Codec(Type.Object({ todo_id: Type.Number() }))
|
|
84
84
|
.Decode((wire) => ({ id: wire.todo_id }))
|
|
85
85
|
.Encode((domain) => ({ todo_id: domain.id }));
|
|
86
86
|
|
|
@@ -88,7 +88,7 @@ const domain = lankaTypeBoxValidator.validate(todoFromApi, wire, "todos.map");
|
|
|
88
88
|
return lankaTypeBoxValidator.validate(todoSchema, domain, "todos.check");
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
A schema without
|
|
91
|
+
A schema without a codec never pays for the decode pass. A decode function
|
|
92
92
|
that throws is a refused body, not a crash.
|
|
93
93
|
|
|
94
94
|
## Never do these
|
|
@@ -101,16 +101,16 @@ that throws is a refused body, not a crash.
|
|
|
101
101
|
- **Never install two validation packages.**
|
|
102
102
|
- **Never expect `validate` to return a result object.** It throws;
|
|
103
103
|
`validateSafe` returns.
|
|
104
|
-
- **Never trust `format
|
|
105
|
-
|
|
106
|
-
register it at start-up.
|
|
104
|
+
- **Never trust a `format` TypeBox does not know.** The standard names are
|
|
105
|
+
built in; any other name — a typo, a custom format never registered — is
|
|
106
|
+
accepted silently and checks nothing. Use `pattern`, or register it at start-up.
|
|
107
107
|
|
|
108
108
|
## Symptom → cause
|
|
109
109
|
|
|
110
110
|
| What you see | What it is |
|
|
111
111
|
| ------------------------------------- | ----------------------------------------------- |
|
|
112
112
|
| validation slower than expected | a schema rebuilt per render — nothing is cached |
|
|
113
|
-
|
|
|
113
|
+
| a `format` that lets everything pass | a name TypeBox does not know, or a typo |
|
|
114
114
|
| a path like `/a/b` reaching your form | not from here — this package returns segments |
|
|
115
115
|
| "invalid response" with no idea which | a label that does not identify the call |
|
|
116
116
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!-- Generated from modules/validators/typebox/GUIDE.md by scripts/skills.mjs. Edit the guide. -->
|
|
2
2
|
|
|
3
|
-
> **`@lankajs/typebox@
|
|
3
|
+
> **`@lankajs/typebox@2.0.0`** — this document describes that version.
|
|
4
4
|
>
|
|
5
|
-
> Install: `npm install @lankajs/typebox
|
|
5
|
+
> Install: `npm install @lankajs/typebox typebox zustand` (the peers are not optional; only npm adds a missing one for you).
|
|
6
6
|
>
|
|
7
7
|
> Complete code, compiled and run in CI: [modules/validators/typebox/_playground/playground.test.ts](https://github.com/lankajs/lanka/blob/main/modules/validators/typebox/_playground/playground.test.ts)
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ compiled checker it caches, plus a `TLankaInferred` helper.
|
|
|
15
15
|
|
|
16
16
|
- why this package is required rather than a matter of taste
|
|
17
17
|
- why your schemas must live at module level, and what it costs when they do not
|
|
18
|
-
- how to map a wire format with `Type.
|
|
18
|
+
- how to map a wire format with `Type.Codec`
|
|
19
19
|
|
|
20
20
|
## When to reach for this
|
|
21
21
|
|
|
@@ -30,9 +30,14 @@ The moment your schemas are TypeBox. Core's validator cannot be handed one.
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
npm install @lankajs/typebox
|
|
33
|
+
npm install @lankajs/typebox typebox zustand
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> This is TypeBox 1.x — the `typebox` package. An application on TypeBox 0.34
|
|
38
|
+
> (`@sinclair/typebox`) stays on `@lankajs/typebox@1`, whose API is the one
|
|
39
|
+
> below with `Type.Transform` in place of `Type.Codec`.
|
|
40
|
+
|
|
36
41
|
> [!IMPORTANT]
|
|
37
42
|
> `zustand` is `lanka`'s own peer: npm adds a missing peer for you and pnpm
|
|
38
43
|
> does not, so the line names it.
|
|
@@ -48,7 +53,7 @@ version of "just use `lankaStandardValidator`" that compiles or runs.
|
|
|
48
53
|
|
|
49
54
|
```ts
|
|
50
55
|
import { lankaTypeBoxValidator } from "@lankajs/typebox";
|
|
51
|
-
import { Type } from "
|
|
56
|
+
import { Type } from "typebox";
|
|
52
57
|
|
|
53
58
|
// at MODULE level — see below, this is not a style point
|
|
54
59
|
const todoSchema = Type.Object({
|
|
@@ -70,7 +75,7 @@ branch to handle. `validateSafe` is for a form, where failure is ordinary.
|
|
|
70
75
|
|
|
71
76
|
## Declare schemas at module level
|
|
72
77
|
|
|
73
|
-
`
|
|
78
|
+
`Compile(schema)` turns a schema into a generated function — the
|
|
74
79
|
fastest validator in JavaScript — and **compiling is the slow part**. This
|
|
75
80
|
package compiles each schema once and keeps the result in a `WeakMap` keyed by
|
|
76
81
|
the schema object.
|
|
@@ -119,10 +124,10 @@ the form's root, not an input named `""`.
|
|
|
119
124
|
## Mapping a wire format
|
|
120
125
|
|
|
121
126
|
A mapping is a schema, not an adapter layer. In TypeBox it is
|
|
122
|
-
`Type.
|
|
127
|
+
`Type.Codec(...).Decode(...)`:
|
|
123
128
|
|
|
124
129
|
```ts
|
|
125
|
-
const todoFromApi = Type.
|
|
130
|
+
const todoFromApi = Type.Codec(
|
|
126
131
|
Type.Object({
|
|
127
132
|
todo_id: Type.Number(),
|
|
128
133
|
is_done: Type.Union([Type.Literal(0), Type.Literal(1)]),
|
|
@@ -135,19 +140,21 @@ const domain = lankaTypeBoxValidator.validate(todoFromApi, wire, "todos.map");
|
|
|
135
140
|
return lankaTypeBoxValidator.validate(todoSchema, domain, "todos.check");
|
|
136
141
|
```
|
|
137
142
|
|
|
138
|
-
A schema **without**
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
A schema **without** a codec never pays for the decode pass: whether it has one
|
|
144
|
+
is asked once and cached beside the compiled checker. A schema with one is
|
|
145
|
+
decoded on a COPY — your body is left as it arrived — and keeps the properties
|
|
146
|
+
the schema did not name, exactly as a schema without a codec does.
|
|
141
147
|
|
|
142
148
|
A decode function that throws is a **refused body**, not a crash — it comes back
|
|
143
149
|
as a failure, because `validateSafe` promises to throw nothing.
|
|
144
150
|
|
|
145
151
|
## Formats are a registry, not a keyword
|
|
146
152
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
TypeBox 1.x checks the standard formats — `email`, `uuid`, `date-time` and the
|
|
154
|
+
rest — by itself. A format name it does NOT know is accepted silently: a typo, or
|
|
155
|
+
a format of your own that was never registered, is a rule that looks present in
|
|
156
|
+
the schema and checks nothing. Register your own formats at start-up, and prefer
|
|
157
|
+
`pattern` for anything that is not a standard name.
|
|
151
158
|
|
|
152
159
|
## Where to validate
|
|
153
160
|
|
|
@@ -165,14 +172,14 @@ level.
|
|
|
165
172
|
**Expecting `validate` to return a result object.** It throws. `validateSafe`
|
|
166
173
|
returns.
|
|
167
174
|
|
|
168
|
-
**Trusting `format`
|
|
175
|
+
**Trusting a `format` TypeBox does not know.** It accepts everything.
|
|
169
176
|
|
|
170
177
|
## Recap
|
|
171
178
|
|
|
172
179
|
- TypeBox has no Standard Schema, so this package is what makes it work with lanka.
|
|
173
180
|
- Schemas at module level: the compiled checker is cached by object identity.
|
|
174
181
|
- Paths come back as segments, with JSON Pointer escapes decoded.
|
|
175
|
-
- A mapping is `Type.
|
|
182
|
+
- A mapping is `Type.Codec`, and only a schema that has one pays for the decode pass.
|
|
176
183
|
|
|
177
184
|
---
|
|
178
185
|
|