@oh-my-pi/omptype 17.2.7 → 17.2.8
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/CHANGELOG.md +23 -0
- package/dist/js/compile.js +414 -219
- package/dist/js/errors.js +236 -41
- package/dist/js/from-json-schema.js +234 -0
- package/dist/js/index.js +1 -0
- package/dist/js/interp.js +514 -132
- package/dist/js/ir.js +972 -202
- package/dist/js/json-schema.js +73 -34
- package/dist/js/keywords.js +108 -1
- package/dist/js/type.js +2156 -172
- package/dist/js/typebox.js +41 -32
- package/dist/js/zod.js +2 -2
- package/dist/types/compile.d.ts +1 -1
- package/dist/types/errors.d.ts +19 -18
- package/dist/types/from-json-schema.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/infer.d.ts +45 -6
- package/dist/types/interp.d.ts +9 -2
- package/dist/types/ir.d.ts +57 -6
- package/dist/types/json-schema.d.ts +10 -1
- package/dist/types/type.d.ts +324 -48
- package/dist/types/typebox.d.ts +78 -50
- package/package.json +5 -1
- package/src/compile.ts +598 -243
- package/src/errors.ts +247 -49
- package/src/from-json-schema.ts +231 -0
- package/src/index.ts +1 -0
- package/src/infer.ts +107 -32
- package/src/interp.ts +457 -118
- package/src/ir.ts +981 -212
- package/src/json-schema.ts +82 -34
- package/src/keywords.ts +111 -1
- package/src/type.ts +2760 -271
- package/src/typebox.ts +141 -98
- package/src/zod.ts +1 -1
package/src/json-schema.ts
CHANGED
|
@@ -3,17 +3,36 @@ import type { IR, PropIR } from "./ir";
|
|
|
3
3
|
export interface JsonSchemaOptions {
|
|
4
4
|
description?: string;
|
|
5
5
|
target?: string;
|
|
6
|
-
dialect?: string;
|
|
6
|
+
dialect?: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Which side of morphs and defaults to describe:
|
|
9
|
+
* - `'input'` — accepted payloads: morphs emit their input shape, defaulted
|
|
10
|
+
* properties are optional (with `default` annotations).
|
|
11
|
+
* - `'output'` — produced values: morphs emit their output shape, defaulted
|
|
12
|
+
* properties are required (always present after validation).
|
|
13
|
+
* Unset keeps the hybrid legacy behavior (morph output, defaults optional).
|
|
14
|
+
*/
|
|
15
|
+
io?: "input" | "output";
|
|
7
16
|
fallback?: (context: { base: Record<string, unknown> }) => unknown;
|
|
8
17
|
}
|
|
9
18
|
|
|
10
19
|
type JsonSchema = Record<string, unknown>;
|
|
11
20
|
|
|
21
|
+
interface EmitCtx {
|
|
22
|
+
options?: JsonSchemaOptions;
|
|
23
|
+
/** Emitted alias definitions, attached as `$defs` on the root schema. */
|
|
24
|
+
defs: Map<string, JsonSchema>;
|
|
25
|
+
/** Alias resolver identity → assigned `$defs` name (cycle-safe). */
|
|
26
|
+
refs: Map<() => IR, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/** Emit the requested JSON Schema dialect represented by an IR tree. */
|
|
13
30
|
export function irToJsonSchema(ir: IR, options?: JsonSchemaOptions): JsonSchema {
|
|
14
|
-
|
|
31
|
+
const ctx: EmitCtx = { options, defs: new Map(), refs: new Map() };
|
|
32
|
+
let schema = emit(ir, ctx);
|
|
33
|
+
if (ctx.defs.size > 0) schema.$defs = Object.fromEntries(ctx.defs);
|
|
15
34
|
if (options?.target === "draft-07") schema = toDraft7(schema);
|
|
16
|
-
const dialect = options?.dialect ?? dialectFor(options?.target);
|
|
35
|
+
const dialect = options?.dialect === null ? undefined : (options?.dialect ?? dialectFor(options?.target));
|
|
17
36
|
if (dialect !== undefined) schema.$schema = dialect;
|
|
18
37
|
if (options?.description !== undefined) schema.description = options.description;
|
|
19
38
|
return schema;
|
|
@@ -22,11 +41,11 @@ export function irToJsonSchema(ir: IR, options?: JsonSchemaOptions): JsonSchema
|
|
|
22
41
|
function dialectFor(target: string | undefined): string | undefined {
|
|
23
42
|
if (target === "draft-2020-12") return "https://json-schema.org/draft/2020-12/schema";
|
|
24
43
|
if (target === "draft-07") return "http://json-schema.org/draft-07/schema#";
|
|
25
|
-
return target
|
|
44
|
+
return target !== undefined && (target.startsWith("http://") || target.startsWith("https://")) ? target : undefined;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
function fallback(schema: JsonSchema,
|
|
29
|
-
const replacement = options?.fallback?.({ base: schema });
|
|
47
|
+
function fallback(schema: JsonSchema, ctx: EmitCtx): JsonSchema {
|
|
48
|
+
const replacement = ctx.options?.fallback?.({ base: schema });
|
|
30
49
|
if (replacement === true) return {};
|
|
31
50
|
if (replacement === false) return { not: {} };
|
|
32
51
|
return typeof replacement === "object" && replacement !== null ? (replacement as JsonSchema) : schema;
|
|
@@ -36,7 +55,11 @@ function toDraft7(schema: JsonSchema): JsonSchema {
|
|
|
36
55
|
const converted: JsonSchema = {};
|
|
37
56
|
for (const key in schema) {
|
|
38
57
|
const value = schema[key];
|
|
39
|
-
if (key === "
|
|
58
|
+
if (key === "$ref" && typeof value === "string") {
|
|
59
|
+
converted.$ref = value.replace("#/$defs/", "#/definitions/");
|
|
60
|
+
} else if (key === "$defs") {
|
|
61
|
+
converted.definitions = toDraft7(value as JsonSchema);
|
|
62
|
+
} else if (key === "prefixItems" && Array.isArray(value)) {
|
|
40
63
|
converted.items = value.map(item =>
|
|
41
64
|
typeof item === "object" && item !== null ? toDraft7(item as JsonSchema) : item,
|
|
42
65
|
);
|
|
@@ -56,14 +79,14 @@ function toDraft7(schema: JsonSchema): JsonSchema {
|
|
|
56
79
|
return converted;
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
function emit(ir: IR,
|
|
82
|
+
function emit(ir: IR, ctx: EmitCtx): JsonSchema {
|
|
60
83
|
let schema: JsonSchema;
|
|
61
84
|
switch (ir.k) {
|
|
62
85
|
case "unknown":
|
|
63
86
|
schema = {};
|
|
64
87
|
break;
|
|
65
88
|
case "undefined":
|
|
66
|
-
schema = fallback({},
|
|
89
|
+
schema = fallback({}, ctx);
|
|
67
90
|
break;
|
|
68
91
|
case "null":
|
|
69
92
|
schema = { type: "null" };
|
|
@@ -75,7 +98,7 @@ function emit(ir: IR, options?: JsonSchemaOptions): JsonSchema {
|
|
|
75
98
|
schema = { type: "integer" };
|
|
76
99
|
break;
|
|
77
100
|
case "symbol":
|
|
78
|
-
schema = fallback({},
|
|
101
|
+
schema = fallback({}, ctx);
|
|
79
102
|
break;
|
|
80
103
|
case "never":
|
|
81
104
|
schema = { not: {} };
|
|
@@ -93,19 +116,19 @@ function emit(ir: IR, options?: JsonSchemaOptions): JsonSchema {
|
|
|
93
116
|
schema = emitLiteral(ir.v);
|
|
94
117
|
break;
|
|
95
118
|
case "union":
|
|
96
|
-
schema = emitUnion(ir.members,
|
|
119
|
+
schema = emitUnion(ir.members, ctx);
|
|
97
120
|
break;
|
|
98
121
|
case "intersection":
|
|
99
|
-
schema = { allOf: ir.members.map(member => emit(member,
|
|
122
|
+
schema = { allOf: ir.members.map(member => emit(member, ctx)) };
|
|
100
123
|
break;
|
|
101
124
|
case "array":
|
|
102
|
-
schema = { type: "array", items: emit(ir.el,
|
|
125
|
+
schema = { type: "array", items: emit(ir.el, ctx) };
|
|
103
126
|
if (ir.min !== undefined) schema.minItems = ir.min;
|
|
104
127
|
if (ir.max !== undefined) schema.maxItems = ir.max;
|
|
105
128
|
break;
|
|
106
129
|
case "tuple": {
|
|
107
130
|
const prefixItems = ir.prefix.map(item => {
|
|
108
|
-
const itemSchema = emit(item.val,
|
|
131
|
+
const itemSchema = emit(item.val, ctx);
|
|
109
132
|
if (item.hasDefault) {
|
|
110
133
|
itemSchema.default = item.defFactory && typeof item.def === "function" ? item.def() : item.def;
|
|
111
134
|
}
|
|
@@ -117,35 +140,57 @@ function emit(ir: IR, options?: JsonSchemaOptions): JsonSchema {
|
|
|
117
140
|
);
|
|
118
141
|
schema = { type: "array", prefixItems, minItems: required };
|
|
119
142
|
if (ir.variadic === undefined) {
|
|
120
|
-
schema.maxItems = ir.prefix.length + ir.postfix.length;
|
|
121
143
|
schema.items = false;
|
|
122
144
|
} else {
|
|
123
|
-
schema.items = emit(ir.variadic,
|
|
145
|
+
schema.items = emit(ir.variadic, ctx);
|
|
124
146
|
}
|
|
125
147
|
break;
|
|
126
148
|
}
|
|
127
149
|
case "object":
|
|
128
|
-
schema = emitObject(ir.props, ir.index, ir.extras,
|
|
150
|
+
schema = emitObject(ir.props, ir.index, ir.extras, ctx);
|
|
129
151
|
break;
|
|
130
152
|
case "instance":
|
|
131
|
-
schema = ir.ctor === Date ? { type: "string", format: "date-time" } : fallback({ type: "object" },
|
|
153
|
+
schema = ir.ctor === Date ? { type: "string", format: "date-time" } : fallback({ type: "object" }, ctx);
|
|
132
154
|
break;
|
|
133
155
|
case "refine":
|
|
134
|
-
schema = emit(ir.base,
|
|
156
|
+
schema = emit(ir.base, ctx);
|
|
135
157
|
if (ir.json !== undefined) Object.assign(schema, ir.json);
|
|
136
158
|
break;
|
|
137
159
|
case "morph":
|
|
138
|
-
schema = fallback(emit(ir.out ?? ir.input,
|
|
160
|
+
schema = ctx.options?.io === "input" ? emit(ir.input, ctx) : fallback(emit(ir.out ?? ir.input, ctx), ctx);
|
|
139
161
|
break;
|
|
140
|
-
case "alias":
|
|
141
|
-
|
|
162
|
+
case "alias": {
|
|
163
|
+
const known = ctx.refs.get(ir.resolve);
|
|
164
|
+
if (known !== undefined) {
|
|
165
|
+
schema = { $ref: `#/$defs/${known}` };
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// Register before lowering so cyclic aliases resolve to the same $ref
|
|
169
|
+
// instead of recursing forever.
|
|
170
|
+
let name = ir.name.replace(/[^\w.-]/g, "_");
|
|
171
|
+
while ([...ctx.refs.values()].includes(name)) name = `${name}_`;
|
|
172
|
+
ctx.refs.set(ir.resolve, name);
|
|
173
|
+
ctx.defs.set(name, emit(ir.resolve(), ctx));
|
|
174
|
+
schema = { $ref: `#/$defs/${name}` };
|
|
142
175
|
break;
|
|
176
|
+
}
|
|
143
177
|
case "sub":
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
if (ctx.options?.io === "output" && ir.schema.opaqueOutput) {
|
|
179
|
+
schema = {};
|
|
180
|
+
} else if (ctx.options?.io === "output" && ir.schema.stepOut !== undefined) {
|
|
181
|
+
schema = emit(ir.schema.stepOut, ctx);
|
|
182
|
+
} else if (ctx.options?.io === "input") {
|
|
183
|
+
schema = emit(ir.schema.ir, ctx);
|
|
184
|
+
} else {
|
|
185
|
+
schema = ir.schema.hasSteps ? fallback(emit(ir.schema.ir, ctx), ctx) : emit(ir.schema.ir, ctx);
|
|
186
|
+
}
|
|
187
|
+
if (ir.schema.ir.desc !== undefined) schema.description = ir.schema.ir.desc;
|
|
188
|
+
else if (ir.schema.description !== undefined && ir.descAuto !== true) {
|
|
189
|
+
schema.description = ir.schema.description;
|
|
190
|
+
}
|
|
146
191
|
break;
|
|
147
192
|
}
|
|
148
|
-
if (ir.desc !== undefined) schema.description = ir.desc;
|
|
193
|
+
if (ir.desc !== undefined && ir.descAuto !== true) schema.description = ir.desc;
|
|
149
194
|
return schema;
|
|
150
195
|
}
|
|
151
196
|
|
|
@@ -184,10 +229,10 @@ function emitLiteral(value: unknown): JsonSchema {
|
|
|
184
229
|
}
|
|
185
230
|
}
|
|
186
231
|
|
|
187
|
-
function emitUnion(members: IR[],
|
|
232
|
+
function emitUnion(members: IR[], ctx: EmitCtx): JsonSchema {
|
|
188
233
|
const defined = members.filter(member => member.k !== "undefined");
|
|
189
234
|
if (defined.length === 0) return {};
|
|
190
|
-
if (defined.length === 1) return emit(defined[0],
|
|
235
|
+
if (defined.length === 1) return emit(defined[0], ctx);
|
|
191
236
|
if (defined.every(member => member.k === "lit" && isJsonValue(member.v))) {
|
|
192
237
|
const values = defined.map(member => (member as Extract<IR, { k: "lit" }>).v);
|
|
193
238
|
const schema: JsonSchema = { enum: values };
|
|
@@ -195,7 +240,7 @@ function emitUnion(members: IR[], options?: JsonSchemaOptions): JsonSchema {
|
|
|
195
240
|
if (scalarType !== undefined) schema.type = scalarType;
|
|
196
241
|
return schema;
|
|
197
242
|
}
|
|
198
|
-
return { anyOf: defined.map(member => emit(member,
|
|
243
|
+
return { anyOf: defined.map(member => emit(member, ctx)) };
|
|
199
244
|
}
|
|
200
245
|
|
|
201
246
|
function homogeneousScalarType(values: unknown[]): string | undefined {
|
|
@@ -223,24 +268,27 @@ function emitObject(
|
|
|
223
268
|
props: PropIR[],
|
|
224
269
|
index: IR | undefined,
|
|
225
270
|
extras: "keep" | "reject" | "delete",
|
|
226
|
-
|
|
271
|
+
ctx: EmitCtx,
|
|
227
272
|
): JsonSchema {
|
|
228
273
|
const properties: Record<string, unknown> = {};
|
|
229
274
|
const required: string[] = [];
|
|
275
|
+
const filled = (prop: PropIR): boolean => !prop.opt && (ctx.options?.io === "output" || !prop.hasDefault);
|
|
230
276
|
// ArkType emits required properties first (each group in declaration
|
|
231
277
|
// order); downstream wire consumers rely on that stable ordering.
|
|
232
|
-
const ordered = [...props.filter(
|
|
278
|
+
const ordered = [...props.filter(filled), ...props.filter(prop => !filled(prop))];
|
|
233
279
|
for (const prop of ordered) {
|
|
234
|
-
|
|
280
|
+
if (typeof prop.key === "symbol") throw new TypeError("Cannot convert a symbol to a string");
|
|
281
|
+
const key = String(prop.key);
|
|
282
|
+
const propertySchema = emit(prop.val, ctx);
|
|
235
283
|
if (prop.hasDefault) {
|
|
236
284
|
propertySchema.default = prop.defFactory ? (prop.def as () => unknown)() : prop.def;
|
|
237
285
|
}
|
|
238
|
-
properties[
|
|
239
|
-
if (
|
|
286
|
+
properties[key] = propertySchema;
|
|
287
|
+
if (filled(prop)) required.push(key);
|
|
240
288
|
}
|
|
241
289
|
const schema: JsonSchema = { type: "object", properties };
|
|
242
290
|
if (required.length > 0) schema.required = required;
|
|
243
|
-
if (index !== undefined) schema.additionalProperties = emit(index,
|
|
291
|
+
if (index !== undefined) schema.additionalProperties = emit(index, ctx);
|
|
244
292
|
else if (extras === "reject") schema.additionalProperties = false;
|
|
245
293
|
return schema;
|
|
246
294
|
}
|
package/src/keywords.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IR, MorphContext } from "./ir";
|
|
2
2
|
|
|
3
|
-
const NUMERIC = /^
|
|
3
|
+
const NUMERIC = /^(?:(?!^-0\.?0*$)(?:-?(?:(?:0|[1-9]\d*)(?:\.\d+)?)|\.\d+?))$/;
|
|
4
4
|
const INTEGER = /^[+-]?(?:0|[1-9]\d*)$/;
|
|
5
5
|
const EMAIL = /^[\w%+.-]+@[\d.A-Za-z-]+\.[A-Za-z]{2,}$/;
|
|
6
6
|
const SEMVER =
|
|
@@ -106,8 +106,118 @@ function isLuhnValid(input: string): boolean {
|
|
|
106
106
|
return value.length > 0 && sum % 10 === 0;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function finiteNumber(expected = "a number"): IR {
|
|
110
|
+
return {
|
|
111
|
+
k: "refine",
|
|
112
|
+
base: { k: "unknown" },
|
|
113
|
+
pred: value => typeof value === "number" && !Number.isNaN(value),
|
|
114
|
+
expected,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function jsonObject(): IR {
|
|
119
|
+
let value: IR;
|
|
120
|
+
const resolveValue = (): IR => value;
|
|
121
|
+
const object: IR = {
|
|
122
|
+
k: "object",
|
|
123
|
+
props: [],
|
|
124
|
+
index: { k: "alias", name: "$jsonValue", resolve: resolveValue },
|
|
125
|
+
extras: "keep",
|
|
126
|
+
};
|
|
127
|
+
const array: IR = {
|
|
128
|
+
k: "array",
|
|
129
|
+
el: { k: "alias", name: "$jsonValue", resolve: resolveValue },
|
|
130
|
+
desc: "an object",
|
|
131
|
+
};
|
|
132
|
+
value = {
|
|
133
|
+
k: "union",
|
|
134
|
+
members: [
|
|
135
|
+
object,
|
|
136
|
+
array,
|
|
137
|
+
{ k: "number" },
|
|
138
|
+
{ k: "string" },
|
|
139
|
+
{ k: "lit", v: false },
|
|
140
|
+
{ k: "null" },
|
|
141
|
+
{ k: "lit", v: true },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
return object;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function instance(ctor: new (...args: never[]) => object, name: string): IR {
|
|
148
|
+
return { k: "instance", ctor, expected: `${/^[AEIOU]/.test(name) ? "an" : "a"} ${name} instance` };
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
const keywordFactories: Record<string, () => IR> = {
|
|
110
152
|
string: () => ({ k: "string" }),
|
|
153
|
+
Key: () => ({ k: "union", members: [{ k: "string" }, { k: "symbol" }] }),
|
|
154
|
+
"unknown.any": () => ({ k: "unknown" }),
|
|
155
|
+
Array: () => ({ k: "array", el: { k: "unknown" } }),
|
|
156
|
+
Function: () => instance(Function as unknown as new (...args: never[]) => object, "Function"),
|
|
157
|
+
RegExp: () => instance(RegExp, "RegExp"),
|
|
158
|
+
File: () => instance(File, "File"),
|
|
159
|
+
Error: () => instance(Error, "Error"),
|
|
160
|
+
Set: () => instance(Set, "Set"),
|
|
161
|
+
Map: () => instance(Map, "Map"),
|
|
162
|
+
WeakSet: () => instance(WeakSet, "WeakSet"),
|
|
163
|
+
WeakMap: () => instance(WeakMap, "WeakMap"),
|
|
164
|
+
Promise: () => instance(Promise, "Promise"),
|
|
165
|
+
FormData: () => instance(FormData, "FormData"),
|
|
166
|
+
"FormData.parse": () => ({
|
|
167
|
+
k: "morph",
|
|
168
|
+
input: instance(FormData, "FormData"),
|
|
169
|
+
fn: value => {
|
|
170
|
+
const out: Record<string, Bun.FormDataEntryValue | Bun.FormDataEntryValue[]> = {};
|
|
171
|
+
for (const [key, entry] of (value as FormData).entries()) {
|
|
172
|
+
const current = out[key];
|
|
173
|
+
out[key] = current === undefined ? entry : Array.isArray(current) ? [...current, entry] : [current, entry];
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
},
|
|
177
|
+
out: { k: "object", props: [], index: { k: "unknown" }, extras: "keep" },
|
|
178
|
+
}),
|
|
179
|
+
"object.json": jsonObject,
|
|
180
|
+
"object.json.stringify": () => ({
|
|
181
|
+
k: "morph",
|
|
182
|
+
input: jsonObject(),
|
|
183
|
+
fn: value => JSON.stringify(value),
|
|
184
|
+
out: { k: "string" },
|
|
185
|
+
}),
|
|
186
|
+
"number.epoch": () => ({
|
|
187
|
+
k: "refine",
|
|
188
|
+
base: {
|
|
189
|
+
k: "refine",
|
|
190
|
+
base: {
|
|
191
|
+
k: "refine",
|
|
192
|
+
base: finiteNumber("a number representing a Unix timestamp"),
|
|
193
|
+
pred: value => Number.isInteger(value),
|
|
194
|
+
expected: "an integer representing a Unix timestamp",
|
|
195
|
+
},
|
|
196
|
+
pred: value => (value as number) >= -8_640_000_000_000_000,
|
|
197
|
+
expected: "a Unix timestamp after -8640000000000000",
|
|
198
|
+
},
|
|
199
|
+
pred: value => (value as number) <= 8_640_000_000_000_000,
|
|
200
|
+
expected: "a Unix timestamp before 8640000000000000",
|
|
201
|
+
}),
|
|
202
|
+
"number.safe": () => ({
|
|
203
|
+
k: "refine",
|
|
204
|
+
base: {
|
|
205
|
+
k: "refine",
|
|
206
|
+
base: finiteNumber(),
|
|
207
|
+
pred: value => (value as number) >= Number.MIN_SAFE_INTEGER,
|
|
208
|
+
expected: `at least ${Number.MIN_SAFE_INTEGER}`,
|
|
209
|
+
},
|
|
210
|
+
pred: value => (value as number) <= Number.MAX_SAFE_INTEGER,
|
|
211
|
+
expected: `at most ${Number.MAX_SAFE_INTEGER}`,
|
|
212
|
+
}),
|
|
213
|
+
"number.NaN": () => ({
|
|
214
|
+
k: "refine",
|
|
215
|
+
base: { k: "unknown" },
|
|
216
|
+
pred: Number.isNaN,
|
|
217
|
+
expected: "NaN",
|
|
218
|
+
}),
|
|
219
|
+
"number.Infinity": () => ({ k: "lit", v: Number.POSITIVE_INFINITY }),
|
|
220
|
+
"number.NegativeInfinity": () => ({ k: "lit", v: Number.NEGATIVE_INFINITY }),
|
|
111
221
|
"string.alpha": () => pattern(/^[A-Za-z]*$/, "only letters"),
|
|
112
222
|
"string.alphanumeric": () => pattern(/^[\dA-Za-z]*$/, "only letters and digits 0-9"),
|
|
113
223
|
"string.hex": () => pattern(/^[\dA-Fa-f]+$/, "hex characters only"),
|