@badrap/valita
Advanced tools
Comparing version 0.0.3 to 0.0.4
{ | ||
"name": "@badrap/valita", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "A validation & parsing library for TypeScript", | ||
"main": "./dist/index.js", | ||
"main": "./dist/main/index.js", | ||
"types": "./dist/types/index.d.ts", | ||
"exports": { | ||
"node": "./dist/node/index.js", | ||
"default": "./dist/main/index.js" | ||
}, | ||
"sideEffects": false, | ||
@@ -14,3 +19,6 @@ "repository": "badrap/valita", | ||
"test": "mocha --require ts-node/register tests/**/*.test.ts", | ||
"build": "rm -rf dist && tsc -p ./tsconfig.build.json", | ||
"build": "rm -rf dist && npm run build:types && npm run build:main && npm run build:node", | ||
"build:types": "tsc -p ./tsconfig.build.json --emitDeclarationOnly --declaration --declarationDir ./dist/types", | ||
"build:main": "tsc -p ./tsconfig.build.json --target es5 --outDir ./dist/default", | ||
"build:node": "tsc -p ./tsconfig.build.json --target es2019 --outDir ./dist/node", | ||
"prepack": "npm run build" | ||
@@ -17,0 +25,0 @@ }, |
@@ -55,3 +55,3 @@ # @badrap/valita [![tests](https://github.com/badrap/valita/workflows/tests/badge.svg)](https://github.com/badrap/valita/actions?query=workflow%3Atests) [![npm](https://img.shields.io/npm/v/@badrap/valita.svg)](https://www.npmjs.com/package/@badrap/valita) | ||
In fact, you can get your mitts on the this type in the code: | ||
You can use `infer<T>` to get your mitts on the inferred type in your code: | ||
@@ -58,0 +58,0 @@ ```ts |
908
src/index.ts
@@ -1,46 +0,71 @@ | ||
type IssueCode = | ||
| "invalid_type" | ||
| "invalid_literal_value" | ||
| "invalid_union" | ||
| "missing_key" | ||
| "unrecognized_key"; | ||
// This is magic that turns object intersections to nicer-looking types. | ||
type PrettyIntersection<V> = Extract<{ [K in keyof V]: V[K] }, unknown>; | ||
type IssuePath = (string | number)[]; | ||
type Literal = string | number | bigint | boolean; | ||
type Key = string | number; | ||
type BaseType = | ||
| "object" | ||
| "array" | ||
| "null" | ||
| "undefined" | ||
| "string" | ||
| "number" | ||
| "bigint" | ||
| "boolean"; | ||
type Issue = { | ||
code: IssueCode; | ||
path: IssuePath; | ||
message: string; | ||
}; | ||
type I<Code, Extra = unknown> = Readonly< | ||
PrettyIntersection< | ||
Extra & { | ||
code: Code; | ||
path?: Key[]; | ||
} | ||
> | ||
>; | ||
function _collectIssues( | ||
ctx: ErrorContext, | ||
path: IssuePath, | ||
issues: Issue[] | ||
): void { | ||
if (ctx.type === "error") { | ||
issues.push({ | ||
code: ctx.code, | ||
path: path.slice(), | ||
message: ctx.message, | ||
}); | ||
type Issue = | ||
| I<"invalid_type", { expected: BaseType[] }> | ||
| I<"invalid_literal", { expected: Literal[] }> | ||
| I<"missing_key", { key: Key }> | ||
| I<"unrecognized_key", { key: Key }> | ||
| I<"invalid_union", { tree: IssueTree }>; | ||
type IssueTree = | ||
| Readonly<{ code: "prepend"; key: Key; tree: IssueTree }> | ||
| Readonly<{ code: "join"; left: IssueTree; right: IssueTree }> | ||
| Issue; | ||
function _collectIssues(tree: IssueTree, path: Key[], issues: Issue[]): void { | ||
if (tree.code === "join") { | ||
_collectIssues(tree.left, path, issues); | ||
_collectIssues(tree.right, path, issues); | ||
} else if (tree.code === "prepend") { | ||
path.push(tree.key); | ||
_collectIssues(tree.tree, path, issues); | ||
path.pop(); | ||
} else { | ||
if (ctx.next) { | ||
_collectIssues(ctx.next, path, issues); | ||
} | ||
path.push(ctx.value); | ||
_collectIssues(ctx.current, path, issues); | ||
path.pop(); | ||
issues.push({ ...tree, path: path.concat(tree.path || []) }); | ||
} | ||
} | ||
function collectIssues(ctx: ErrorContext): Issue[] { | ||
function collectIssues(tree: IssueTree): Issue[] { | ||
const issues: Issue[] = []; | ||
const path: IssuePath = []; | ||
_collectIssues(ctx, path, issues); | ||
const path: Key[] = []; | ||
_collectIssues(tree, path, issues); | ||
return issues; | ||
} | ||
function orList(list: string[]): string { | ||
const last = list[list.length - 1]; | ||
if (list.length < 2) { | ||
return last; | ||
} | ||
return `${list.slice(0, -1).join(", ")} or ${last}`; | ||
} | ||
function formatLiteral(value: Literal): string { | ||
return typeof value === "bigint" ? `${value}n` : JSON.stringify(value); | ||
} | ||
export class ValitaError extends Error { | ||
constructor(private readonly ctx: ErrorContext) { | ||
constructor(private readonly issueTree: IssueTree) { | ||
super(); | ||
@@ -52,3 +77,3 @@ Object.setPrototypeOf(this, new.target.prototype); | ||
get issues(): readonly Issue[] { | ||
const issues = collectIssues(this.ctx); | ||
const issues = collectIssues(this.issueTree); | ||
Object.defineProperty(this, "issues", { | ||
@@ -60,60 +85,76 @@ value: issues, | ||
} | ||
get message(): string { | ||
const issue = this.issues[0]; | ||
let message = "invalid value"; | ||
if (issue.code === "invalid_type") { | ||
message = `expected ${orList(issue.expected)}`; | ||
} else if (issue.code === "invalid_literal") { | ||
message = `expected ${orList(issue.expected.map(formatLiteral))}`; | ||
} else if (issue.code === "missing_key") { | ||
message = `missing key ${formatLiteral(issue.key)}`; | ||
} else if (issue.code === "unrecognized_key") { | ||
message = `unrecognized key ${formatLiteral(issue.key)}`; | ||
} | ||
const path = "." + (issue.path || []).join("."); | ||
return `${issue.code} at ${path} (${message})`; | ||
} | ||
} | ||
function isObject(v: unknown): v is Record<string, unknown> { | ||
return typeof v === "object" && v !== null && !Array.isArray(v); | ||
function joinIssues(left: IssueTree, right: IssueTree | undefined): IssueTree { | ||
return right ? { code: "join", left, right } : left; | ||
} | ||
type PrettifyObjectType<V> = Extract<{ [K in keyof V]: V[K] }, unknown>; | ||
function prependPath(key: Key, tree: IssueTree): IssueTree { | ||
return { code: "prepend", key, tree }; | ||
} | ||
type ErrorContext = Readonly< | ||
| { | ||
ok: false; | ||
type: "path"; | ||
value: string | number; | ||
current: ErrorContext; | ||
next?: ErrorContext; | ||
} | ||
| { | ||
ok: false; | ||
type: "error"; | ||
code: IssueCode; | ||
message: string; | ||
} | ||
>; | ||
type Ok<T> = | ||
| true | ||
| Readonly<{ | ||
ok: true; | ||
code: "ok"; | ||
value: T; | ||
}>; | ||
type Result<T> = Ok<T> | ErrorContext; | ||
type Result<T> = Ok<T> | IssueTree; | ||
function err(code: IssueCode, message: string): ErrorContext { | ||
return { ok: false, type: "error", code, message }; | ||
function isObject(v: unknown): v is Record<string, unknown> { | ||
return typeof v === "object" && v !== null && !Array.isArray(v); | ||
} | ||
function appendErr( | ||
to: ErrorContext | undefined, | ||
key: string | number, | ||
err: ErrorContext | ||
): ErrorContext { | ||
return { | ||
ok: false, | ||
type: "path", | ||
value: key, | ||
current: err, | ||
next: to, | ||
}; | ||
function toTerminals(type: Type): TerminalType[] { | ||
const result: TerminalType[] = []; | ||
type.toTerminals(result); | ||
return result; | ||
} | ||
type Infer<T extends Vx<unknown>> = T extends Vx<infer I> ? I : never; | ||
type Infer<T extends Type> = T extends Type<infer I> ? I : never; | ||
class Vx<T> { | ||
constructor( | ||
private readonly genFunc: () => (v: unknown) => Result<T>, | ||
readonly isOptional: boolean | ||
) {} | ||
const enum FuncMode { | ||
PASS = 0, | ||
STRICT = 1, | ||
STRIP = 2, | ||
} | ||
type Func<T> = (v: unknown, mode: FuncMode) => Result<T>; | ||
get func(): (v: unknown) => Result<T> { | ||
type ParseOptions = { | ||
mode: "passthrough" | "strict" | "strip"; | ||
}; | ||
abstract class Type<Out = unknown> { | ||
abstract readonly name: string; | ||
abstract genFunc(): Func<Out>; | ||
abstract toTerminals(into: TerminalType[]): void; | ||
get isOptional(): boolean { | ||
const isOptional = toTerminals(this).some((t) => t.name === "undefined"); | ||
Object.defineProperty(this, "isOptional", { | ||
value: isOptional, | ||
writable: false, | ||
}); | ||
return isOptional; | ||
} | ||
get func(): Func<Out> { | ||
const f = this.genFunc(); | ||
@@ -127,21 +168,14 @@ Object.defineProperty(this, "func", { | ||
transform<O>(func: (v: T) => Result<O>): Vx<O> { | ||
const f = this.func; | ||
return new Vx( | ||
() => (v) => { | ||
const r = f(v); | ||
if (r !== true && !r.ok) { | ||
return r; | ||
} | ||
return func(r === true ? (v as T) : r.value); | ||
}, | ||
this.isOptional | ||
); | ||
} | ||
parse(v: unknown, options?: Partial<ParseOptions>): Out { | ||
let mode: FuncMode = FuncMode.PASS; | ||
if (options && options.mode === "strict") { | ||
mode = FuncMode.STRICT; | ||
} else if (options && options.mode === "strip") { | ||
mode = FuncMode.STRIP; | ||
} | ||
parse(v: unknown): T { | ||
const r = this.func(v); | ||
const r = this.func(v, mode); | ||
if (r === true) { | ||
return v as T; | ||
} else if (r.ok) { | ||
return v as Out; | ||
} else if (r.code === "ok") { | ||
return r.value; | ||
@@ -153,86 +187,86 @@ } else { | ||
optional(): Vx<T | undefined> { | ||
const f = this.func; | ||
return new Vx( | ||
() => (v) => { | ||
return v === undefined ? true : f(v); | ||
}, | ||
true | ||
); | ||
optional(): OptionalType<Out, undefined> { | ||
return new OptionalType(this, undefined); | ||
} | ||
transform<T>(this: Type, func: (v: Out) => Result<T>): TransformType<T> { | ||
return new TransformType(this, func as (v: unknown) => Result<T>); | ||
} | ||
} | ||
type Optionals<T extends Record<string, Vx<unknown>>> = { | ||
type Optionals<T extends Record<string, Type>> = { | ||
[K in keyof T]: undefined extends Infer<T[K]> ? K : never; | ||
}[keyof T]; | ||
type UnknownKeys = "passthrough" | "strict" | "strip" | Vx<unknown>; | ||
type ObjectShape = Record<string, Type>; | ||
type VxObjOutput< | ||
T extends Record<string, Vx<unknown>>, | ||
U extends UnknownKeys | ||
> = PrettifyObjectType< | ||
type ObjectOutput< | ||
T extends ObjectShape, | ||
R extends Type | undefined | ||
> = PrettyIntersection< | ||
{ [K in Optionals<T>]?: Infer<T[K]> } & | ||
{ [K in Exclude<keyof T, Optionals<T>>]: Infer<T[K]> } & | ||
(U extends "passthrough" ? { [K: string]: unknown } : unknown) & | ||
(U extends Vx<infer C> ? { [K: string]: C } : unknown) | ||
(R extends Type ? { [K: string]: Infer<R> } : unknown) | ||
>; | ||
class VxObj< | ||
T extends Record<string, Vx<unknown>>, | ||
U extends UnknownKeys | ||
> extends Vx<VxObjOutput<T, U>> { | ||
constructor(private readonly shape: T, private readonly unknownKeys: U) { | ||
super(() => { | ||
const shape = this.shape; | ||
const strip = this.unknownKeys === "strip"; | ||
const strict = this.unknownKeys === "strict"; | ||
const passthrough = this.unknownKeys === "passthrough"; | ||
const catchall = | ||
this.unknownKeys instanceof Vx | ||
? (this.unknownKeys.func as (v: unknown) => Result<unknown>) | ||
: undefined; | ||
class ObjectType< | ||
T extends ObjectShape = ObjectShape, | ||
Rest extends Type | undefined = Type | undefined | ||
> extends Type<ObjectOutput<T, Rest>> { | ||
readonly name = "object"; | ||
const keys: string[] = []; | ||
const funcs: ((v: unknown) => Result<unknown>)[] = []; | ||
const required: boolean[] = []; | ||
const knownKeys = Object.create(null); | ||
const shapeTemplate = {} as Record<string, unknown>; | ||
for (const key in shape) { | ||
keys.push(key); | ||
funcs.push(shape[key].func); | ||
required.push(!shape[key].isOptional); | ||
knownKeys[key] = true; | ||
shapeTemplate[key] = undefined; | ||
constructor(readonly shape: T, private readonly restType: Rest) { | ||
super(); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
genFunc(): Func<ObjectOutput<T, Rest>> { | ||
const shape = this.shape; | ||
const rest = this.restType ? this.restType.func : undefined; | ||
const keys: string[] = []; | ||
const funcs: Func<unknown>[] = []; | ||
const required: boolean[] = []; | ||
const knownKeys = Object.create(null); | ||
const shapeTemplate = {} as Record<string, unknown>; | ||
for (const key in shape) { | ||
keys.push(key); | ||
funcs.push(shape[key].func); | ||
required.push(!shape[key].isOptional); | ||
knownKeys[key] = true; | ||
shapeTemplate[key] = undefined; | ||
} | ||
return (obj, mode) => { | ||
if (!isObject(obj)) { | ||
return { code: "invalid_type", expected: ["object"] }; | ||
} | ||
const pass = mode === FuncMode.PASS; | ||
const strict = mode === FuncMode.STRICT; | ||
const strip = mode === FuncMode.STRIP; | ||
const template = pass || rest ? obj : shapeTemplate; | ||
return (obj) => { | ||
if (!isObject(obj)) { | ||
return err("invalid_type", "expected an object"); | ||
} | ||
let ctx: ErrorContext | undefined = undefined; | ||
let output: Record<string, unknown> = obj; | ||
const template = strict || strip ? shapeTemplate : obj; | ||
if (!passthrough) { | ||
for (const key in obj) { | ||
if (!knownKeys[key]) { | ||
if (strict) { | ||
return err( | ||
"unrecognized_key", | ||
`unrecognized key ${JSON.stringify(key)}` | ||
); | ||
} else if (strip) { | ||
output = { ...template }; | ||
break; | ||
} else if (catchall) { | ||
const r = catchall(obj[key]); | ||
if (r !== true) { | ||
if (r.ok) { | ||
if (output === obj) { | ||
output = { ...template }; | ||
} | ||
output[key] = r.value; | ||
} else { | ||
ctx = appendErr(ctx, key, r); | ||
let issueTree: IssueTree | undefined = undefined; | ||
let output: Record<string, unknown> = obj; | ||
if (strict || strip || rest) { | ||
for (const key in obj) { | ||
if (!knownKeys[key]) { | ||
if (strict) { | ||
return { code: "unrecognized_key", key }; | ||
} else if (strip) { | ||
output = { ...template }; | ||
break; | ||
} else if (rest) { | ||
const r = rest(obj[key], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === obj) { | ||
output = { ...template }; | ||
} | ||
output[key] = r.value; | ||
} else { | ||
issueTree = joinIssues(prependPath(key, r), issueTree); | ||
} | ||
@@ -243,137 +277,481 @@ } | ||
} | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = obj[key]; | ||
} | ||
if (value === undefined && required[i]) { | ||
ctx = appendErr(ctx, key, err("missing_key", `missing key`)); | ||
} else { | ||
const r = funcs[i](value); | ||
if (r !== true) { | ||
if (r.ok) { | ||
if (output === obj) { | ||
output = { ...template }; | ||
} | ||
output[keys[i]] = r.value; | ||
} else { | ||
ctx = appendErr(ctx, key, r); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = obj[key]; | ||
if (value === undefined && required[i]) { | ||
return { code: "missing_key", key }; | ||
} else { | ||
const r = funcs[i](value, mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === obj) { | ||
output = { ...template }; | ||
} | ||
output[keys[i]] = r.value; | ||
} else { | ||
issueTree = joinIssues(prependPath(key, r), issueTree); | ||
} | ||
} | ||
} | ||
} | ||
if (ctx) { | ||
return ctx; | ||
} else if (obj === output) { | ||
return true; | ||
} else { | ||
return { ok: true, value: output as VxObjOutput<T, U> }; | ||
} | ||
}; | ||
}, false); | ||
if (issueTree) { | ||
return issueTree; | ||
} else if (obj === output) { | ||
return true; | ||
} else { | ||
return { code: "ok", value: output as ObjectOutput<T, Rest> }; | ||
} | ||
}; | ||
} | ||
passthrough(): VxObj<T, "passthrough"> { | ||
return new VxObj(this.shape, "passthrough"); | ||
rest<R extends Type>(restType: R): ObjectType<T, R> { | ||
return new ObjectType(this.shape, restType); | ||
} | ||
strict(): VxObj<T, "strict"> { | ||
return new VxObj(this.shape, "strict"); | ||
} | ||
class ArrayType<T extends Type = Type> extends Type<Infer<T>[]> { | ||
readonly name = "array"; | ||
constructor(readonly item: T) { | ||
super(); | ||
} | ||
strip(): VxObj<T, "strip"> { | ||
return new VxObj(this.shape, "strip"); | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
catchall<C extends Vx<unknown>>(catchall: C): VxObj<T, C> { | ||
return new VxObj(this.shape, catchall); | ||
} | ||
} | ||
class VxArr<T extends Vx<unknown>> extends Vx<Infer<T>[]> { | ||
constructor(private readonly item: T) { | ||
super(() => { | ||
const func = this.item.func; | ||
return (arr) => { | ||
if (!Array.isArray(arr)) { | ||
return err("invalid_type", "expected an array"); | ||
} | ||
let ctx: ErrorContext | undefined = undefined; | ||
let output: Infer<T>[] = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const r = func(arr[i]); | ||
if (r !== true) { | ||
if (r.ok) { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value as Infer<T>; | ||
} else { | ||
ctx = appendErr(ctx, i, r); | ||
genFunc(): Func<Infer<T>[]> { | ||
const func = this.item.func; | ||
return (arr, mode) => { | ||
if (!Array.isArray(arr)) { | ||
return { code: "invalid_type", expected: ["array"] }; | ||
} | ||
let issueTree: IssueTree | undefined = undefined; | ||
let output: Infer<T>[] = arr; | ||
for (let i = 0; i < arr.length; i++) { | ||
const r = func(arr[i], mode); | ||
if (r !== true) { | ||
if (r.code === "ok") { | ||
if (output === arr) { | ||
output = arr.slice(); | ||
} | ||
output[i] = r.value as Infer<T>; | ||
} else { | ||
issueTree = joinIssues(prependPath(i, r), issueTree); | ||
} | ||
} | ||
if (ctx) { | ||
return ctx; | ||
} else if (arr === output) { | ||
return true; | ||
} | ||
if (issueTree) { | ||
return issueTree; | ||
} else if (arr === output) { | ||
return true; | ||
} else { | ||
return { code: "ok", value: output }; | ||
} | ||
}; | ||
} | ||
} | ||
function toBaseType(v: unknown): BaseType { | ||
const type = typeof v; | ||
if (type !== "object") { | ||
return type as BaseType; | ||
} else if (v === null) { | ||
return "null"; | ||
} else if (Array.isArray(v)) { | ||
return "array"; | ||
} else { | ||
return type; | ||
} | ||
} | ||
function dedup<T>(arr: T[]): T[] { | ||
const output = []; | ||
const seen = new Set(); | ||
for (let i = 0; i < arr.length; i++) { | ||
if (!seen.has(arr[i])) { | ||
output.push(arr[i]); | ||
seen.add(arr[i]); | ||
} | ||
} | ||
return output; | ||
} | ||
function findCommonKeys(rs: ObjectShape[]): string[] { | ||
const map = new Map<string, number>(); | ||
rs.forEach((r) => { | ||
for (const key in r) { | ||
map.set(key, (map.get(key) || 0) + 1); | ||
} | ||
}); | ||
const result = [] as string[]; | ||
map.forEach((count, key) => { | ||
if (count === rs.length) { | ||
result.push(key); | ||
} | ||
}); | ||
return result; | ||
} | ||
function createObjectMatchers( | ||
t: { root: Type; terminal: TerminalType }[] | ||
): { | ||
key: string; | ||
isOptional: boolean; | ||
matcher: ( | ||
rootValue: unknown, | ||
value: unknown, | ||
mode: FuncMode | ||
) => Result<unknown>; | ||
}[] { | ||
const objects: { | ||
root: Type; | ||
terminal: TerminalType & { name: "object" }; | ||
}[] = []; | ||
t.forEach(({ root, terminal }) => { | ||
if (terminal.name === "object") { | ||
objects.push({ root, terminal }); | ||
} | ||
}); | ||
const shapes = objects.map(({ terminal }) => terminal.shape); | ||
const common = findCommonKeys(shapes); | ||
const discriminants = common.filter((key) => { | ||
const types = new Map<BaseType, unknown[]>(); | ||
const literals = new Map<unknown, unknown[]>(); | ||
shapes.forEach((shape) => { | ||
toTerminals(shape[key]).forEach((terminal) => { | ||
if (terminal.name === "literal") { | ||
const options = literals.get(terminal.value) || []; | ||
options.push(shape); | ||
literals.set(terminal.value, options); | ||
} else { | ||
return { ok: true, value: output }; | ||
const options = types.get(terminal.name) || []; | ||
options.push(shape); | ||
types.set(terminal.name, options); | ||
} | ||
}; | ||
}, false); | ||
}); | ||
}); | ||
literals.forEach((found, value) => { | ||
const options = types.get(toBaseType(value)); | ||
if (options) { | ||
options.push(...found); | ||
literals.delete(value); | ||
} | ||
}); | ||
let success = true; | ||
literals.forEach((found) => { | ||
if (dedup(found).length > 1) { | ||
success = false; | ||
} | ||
}); | ||
types.forEach((found) => { | ||
if (dedup(found).length > 1) { | ||
success = false; | ||
} | ||
}); | ||
return success; | ||
}); | ||
return discriminants.map((key) => { | ||
const flattened = flatten( | ||
objects.map(({ root, terminal }) => ({ | ||
root, | ||
type: terminal.shape[key], | ||
})) | ||
); | ||
return { | ||
key, | ||
matcher: createUnionMatcher(flattened), | ||
isOptional: objects.some( | ||
({ terminal }) => terminal.shape[key].isOptional | ||
), | ||
}; | ||
}); | ||
} | ||
function createUnionMatcher( | ||
t: { root: Type; terminal: TerminalType }[] | ||
): (rootValue: unknown, value: unknown, mode: FuncMode) => Result<unknown> { | ||
const literals = new Map<unknown, Type[]>(); | ||
const types = new Map<BaseType, Type[]>(); | ||
const allTypes = new Set<BaseType>(); | ||
t.forEach(({ root, terminal }) => { | ||
if (terminal.name === "literal") { | ||
const roots = literals.get(terminal.value) || []; | ||
roots.push(root); | ||
literals.set(terminal.value, roots); | ||
allTypes.add(toBaseType(terminal.value)); | ||
} else { | ||
const roots = types.get(terminal.name) || []; | ||
roots.push(root); | ||
types.set(terminal.name, roots); | ||
allTypes.add(terminal.name); | ||
} | ||
}); | ||
literals.forEach((vxs, value) => { | ||
const options = types.get(toBaseType(value)); | ||
if (options) { | ||
options.push(...vxs); | ||
literals.delete(value); | ||
} | ||
}); | ||
types.forEach((roots, type) => types.set(type, dedup(roots))); | ||
literals.forEach((roots, value) => literals.set(value, dedup(roots))); | ||
const expectedTypes: BaseType[] = []; | ||
allTypes.forEach((type) => expectedTypes.push(type)); | ||
const expectedLiterals: Literal[] = []; | ||
literals.forEach((_, value) => { | ||
expectedLiterals.push(value as Literal); | ||
}); | ||
const invalidType: Issue = { | ||
code: "invalid_type", | ||
expected: expectedTypes, | ||
}; | ||
const invalidLiteral: Issue = { | ||
code: "invalid_literal", | ||
expected: expectedLiterals, | ||
}; | ||
return (rootValue, value, mode) => { | ||
const type = toBaseType(value); | ||
if (!allTypes.has(type)) { | ||
return invalidType; | ||
} | ||
const options = literals.get(value) || types.get(type); | ||
if (options) { | ||
let issueTree: IssueTree | undefined; | ||
for (let i = 0; i < options.length; i++) { | ||
const r = options[i].func(rootValue, mode); | ||
if (r === true || r.code === "ok") { | ||
return r; | ||
} | ||
issueTree = joinIssues(r, issueTree); | ||
} | ||
if (issueTree) { | ||
if (options.length > 1) { | ||
return { code: "invalid_union", tree: issueTree }; | ||
} | ||
return issueTree; | ||
} | ||
} | ||
return invalidLiteral; | ||
}; | ||
} | ||
function flatten( | ||
t: { root: Type; type: Type }[] | ||
): { root: Type; terminal: TerminalType }[] { | ||
const result: { root: Type; terminal: TerminalType }[] = []; | ||
t.forEach(({ root, type }) => | ||
toTerminals(type).forEach((terminal) => { | ||
result.push({ root, terminal }); | ||
}) | ||
); | ||
return result; | ||
} | ||
class UnionType<T extends Type[] = Type[]> extends Type<Infer<T[number]>> { | ||
readonly name = "union"; | ||
constructor(readonly options: T) { | ||
super(); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
this.options.forEach((o) => o.toTerminals(into)); | ||
} | ||
genFunc(): Func<Infer<T[number]>> { | ||
const flattened = flatten( | ||
this.options.map((root) => ({ root, type: root })) | ||
); | ||
const objects = createObjectMatchers(flattened); | ||
const base = createUnionMatcher(flattened); | ||
return (v, mode) => { | ||
if (objects.length > 0 && isObject(v)) { | ||
const item = objects[0]; | ||
const value = v[item.key]; | ||
if (value === undefined && !item.isOptional && !(item.key in v)) { | ||
return { code: "missing_key", key: item.key }; | ||
} | ||
const r = item.matcher(v, value, mode); | ||
if (r === true || r.code === "ok") { | ||
return r as Result<Infer<T[number]>>; | ||
} | ||
return prependPath(item.key, r); | ||
} | ||
return base(v, v, mode) as Result<Infer<T[number]>>; | ||
}; | ||
} | ||
} | ||
function number(): Vx<number> { | ||
const e = err("invalid_type", "expected a number"); | ||
return new Vx(() => (v) => (typeof v === "number" ? true : e), false); | ||
class NumberType extends Type<number> { | ||
readonly name = "number"; | ||
genFunc(): Func<number> { | ||
const issue: Issue = { code: "invalid_type", expected: ["number"] }; | ||
return (v, _mode) => (typeof v === "number" ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function bigint(): Vx<bigint> { | ||
const e = err("invalid_type", "expected a bigint"); | ||
return new Vx(() => (v) => (typeof v === "bigint" ? true : e), false); | ||
class StringType extends Type<number> { | ||
readonly name = "string"; | ||
genFunc(): Func<number> { | ||
const issue: Issue = { code: "invalid_type", expected: ["string"] }; | ||
return (v, _mode) => (typeof v === "string" ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function string(): Vx<string> { | ||
const e = err("invalid_type", "expected a string"); | ||
return new Vx(() => (v) => (typeof v === "string" ? true : e), false); | ||
class BigIntType extends Type<number> { | ||
readonly name = "bigint"; | ||
genFunc(): Func<number> { | ||
const issue: Issue = { code: "invalid_type", expected: ["bigint"] }; | ||
return (v, _mode) => (typeof v === "bigint" ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function boolean(): Vx<boolean> { | ||
const e = err("invalid_type", "expected a boolean"); | ||
return new Vx(() => (v) => (typeof v === "boolean" ? true : e), false); | ||
class BooleanType extends Type<number> { | ||
readonly name = "boolean"; | ||
genFunc(): Func<number> { | ||
const issue: Issue = { code: "invalid_type", expected: ["boolean"] }; | ||
return (v, _mode) => (typeof v === "boolean" ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function object<T extends Record<string, Vx<unknown>>>( | ||
obj: T | ||
): VxObj<T, "strict"> { | ||
return new VxObj(obj, "strict"); | ||
class UndefinedType extends Type<undefined> { | ||
readonly name = "undefined"; | ||
genFunc(): Func<undefined> { | ||
const issue: Issue = { code: "invalid_type", expected: ["undefined"] }; | ||
return (v, _mode) => (v === undefined ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function array<T extends Vx<unknown>>(item: T): VxArr<T> { | ||
return new VxArr(item); | ||
class NullType extends Type<null> { | ||
readonly name = "null"; | ||
genFunc(): Func<null> { | ||
const issue: Issue = { code: "invalid_type", expected: ["null"] }; | ||
return (v, _mode) => (v === null ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function literal<T extends string | number | boolean | bigint>( | ||
value: T | ||
): Vx<T> { | ||
const exp = typeof value === "bigint" ? `${value}n` : JSON.stringify(value); | ||
const e = err("invalid_literal_value", `expected ${exp}`); | ||
return new Vx(() => (v) => (v === value ? true : e), false); | ||
class LiteralType<Out extends Literal = Literal> extends Type<Out> { | ||
readonly name = "literal"; | ||
constructor(readonly value: Out) { | ||
super(); | ||
} | ||
genFunc(): Func<Out> { | ||
const value = this.value; | ||
const issue: Issue = { code: "invalid_literal", expected: [value] }; | ||
return (v, _) => (v === value ? true : issue); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(this); | ||
} | ||
} | ||
function undefined_(): Vx<undefined> { | ||
const e = err("invalid_type", "expected undefined"); | ||
return new Vx(() => (v) => (v === undefined ? true : e), true); | ||
class OptionalType<Out, Default> extends Type<Out | Default> { | ||
readonly name = "optional"; | ||
constructor( | ||
private readonly type: Type<Out>, | ||
private readonly defaultValue: Default | ||
) { | ||
super(); | ||
} | ||
genFunc(): Func<Out | Default> { | ||
const func = this.type.func; | ||
const defaultResult = | ||
this.defaultValue === undefined | ||
? true | ||
: ({ code: "ok", value: this.defaultValue } as const); | ||
return (v, mode) => (v === undefined ? defaultResult : func(v, mode)); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
into.push(undefined_()); | ||
this.type.toTerminals(into); | ||
} | ||
} | ||
function null_(): Vx<null> { | ||
const e = err("invalid_type", "expected null"); | ||
return new Vx(() => (v) => (v === null ? true : e), false); | ||
} | ||
function union<T extends Vx<unknown>[]>(...args: T): Vx<Infer<T[number]>> { | ||
return new Vx(() => { | ||
const error = err("invalid_union", "invalid union"); | ||
const funcs = args.map((arg) => arg.func); | ||
return (v) => { | ||
for (let i = 0; i < args.length; i++) { | ||
const r = funcs[i](v); | ||
if (r === true || r.ok) { | ||
return r as Result<Infer<T[number]>>; | ||
} | ||
class TransformType<Out> extends Type<Out> { | ||
readonly name = "transform"; | ||
constructor( | ||
readonly transformed: Type, | ||
private readonly transformFunc: (v: unknown) => Result<Out> | ||
) { | ||
super(); | ||
} | ||
genFunc(): Func<Out> { | ||
const f = this.transformed.func; | ||
const t = this.transformFunc; | ||
return (v, mode) => { | ||
const r = f(v, mode); | ||
if (r !== true && r.code !== "ok") { | ||
return r; | ||
} | ||
return error; | ||
return t(r === true ? v : r.value); | ||
}; | ||
}, false); | ||
} | ||
toTerminals(into: TerminalType[]): void { | ||
this.transformed.toTerminals(into); | ||
} | ||
} | ||
function number(): NumberType { | ||
return new NumberType(); | ||
} | ||
function bigint(): BigIntType { | ||
return new BigIntType(); | ||
} | ||
function string(): StringType { | ||
return new StringType(); | ||
} | ||
function boolean(): BooleanType { | ||
return new BooleanType(); | ||
} | ||
function undefined_(): UndefinedType { | ||
return new UndefinedType(); | ||
} | ||
function null_(): NullType { | ||
return new NullType(); | ||
} | ||
function object<T extends Record<string, Type>>( | ||
obj: T | ||
): ObjectType<T, undefined> { | ||
return new ObjectType(obj, undefined); | ||
} | ||
function array<T extends Type>(item: T): ArrayType<T> { | ||
return new ArrayType(item); | ||
} | ||
function literal<T extends Literal>(value: T): LiteralType<T> { | ||
return new LiteralType(value); | ||
} | ||
function union<T extends Type[]>(...options: T): UnionType<T> { | ||
return new UnionType(options); | ||
} | ||
type TerminalType = | ||
| StringType | ||
| NumberType | ||
| BigIntType | ||
| BooleanType | ||
| UndefinedType | ||
| NullType | ||
| ObjectType | ||
| ArrayType | ||
| LiteralType; | ||
export { | ||
@@ -380,0 +758,0 @@ number, |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
74688
7
2248