Skip to content

Commit 294b5c6

Browse files
committed
feat(validation): add validation functions
1 parent 42a1238 commit 294b5c6

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

validation.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2023-latest Tomoki Miyauchi. All rights reserved. MIT license.
2+
// This module is browser compatible.
3+
4+
import { isEmpty, isString } from "./deps.ts";
5+
import {
6+
Err,
7+
Ok,
8+
Result,
9+
Validation,
10+
ValidationError,
11+
Validator,
12+
} from "./types.ts";
13+
import { take } from "./utils.ts";
14+
15+
/** Whether the input satisfy the schema or not. */
16+
export function is<In = unknown, In_ extends In = In>(
17+
validator: Validation<In, In_>,
18+
input: In,
19+
): input is In_ {
20+
const iterable = validator.validate(input);
21+
22+
return isEmpty(iterable);
23+
}
24+
25+
export interface AssertOptions extends ErrorOptions {
26+
message?: string;
27+
28+
/**
29+
* @default "input"
30+
*/
31+
objectName?: string;
32+
33+
/** Whether release internal stack trace or not.
34+
* @default false
35+
*/
36+
releaseStackTrace?: boolean;
37+
}
38+
39+
export interface SingleAssertOptions extends AssertOptions {
40+
once: true;
41+
error?: ErrorConstructor;
42+
}
43+
44+
export interface MultipleAssertOptions extends AssertOptions {
45+
error?: AggregateErrorConstructor;
46+
}
47+
48+
/** Assert schema.
49+
* @throws {ValidationError}
50+
*/
51+
export function assert<In = unknown, In_ extends In = In>(
52+
validator: Validation<In, In_>,
53+
input: In,
54+
options?: SingleAssertOptions,
55+
): asserts input is In_;
56+
57+
/**
58+
* @throws {AggregateError}
59+
*/
60+
export function assert<In = unknown, In_ extends In = In>(
61+
validator: Validation<In, In_>,
62+
input: In,
63+
options?: MultipleAssertOptions,
64+
): asserts input is In_;
65+
66+
export function assert(
67+
validator: Validator,
68+
input: unknown,
69+
options: SingleAssertOptions | MultipleAssertOptions = {},
70+
): asserts input {
71+
const {
72+
message,
73+
cause,
74+
objectName: rootName = "input",
75+
releaseStackTrace = false,
76+
} = options;
77+
const hasOnce = "once" in options;
78+
const maxErrors = hasOnce ? 1 : undefined;
79+
const result = validate(validator, input, { maxErrors });
80+
81+
if (result.isOk()) return;
82+
83+
if (hasOnce) {
84+
const e = result.errors[0]!;
85+
const {
86+
error,
87+
message = makeMsg(e, { rootName }),
88+
cause,
89+
} = options;
90+
91+
if (error) throw captured(new error(message, { cause }));
92+
93+
e.cause = cause;
94+
95+
if (isString(message)) e.message = message;
96+
97+
throw captured(e);
98+
}
99+
100+
options.error ??= AggregateError;
101+
102+
throw captured(
103+
new options.error(
104+
result.errors.map((e) => exposePath(e, rootName)).map(captured),
105+
message,
106+
{ cause },
107+
),
108+
);
109+
110+
// deno-lint-ignore ban-types
111+
function captured<T extends Object>(error: T): T {
112+
if (releaseStackTrace) return error;
113+
114+
Error.captureStackTrace(error, assert);
115+
116+
return error;
117+
}
118+
}
119+
120+
export interface ValidateOptions extends ErrorOptions {
121+
/**
122+
* @default Infinity
123+
*/
124+
maxErrors?: number;
125+
}
126+
127+
/**
128+
* @throws {RangeError} If the {@link ValidateOptions.maxErrors} is not positive integer.
129+
*/
130+
export function validate<In = unknown, In_ extends In = In>(
131+
validation: Validation<In, In_>,
132+
input: In,
133+
options: ValidateOptions = {},
134+
): Result<In_> {
135+
const errors = [...take(validation.validate(input), options.maxErrors)]
136+
.map(setCause);
137+
138+
if (!errors.length) return new Ok(input as In_);
139+
140+
return new Err(errors);
141+
142+
function setCause<T extends Error>(error: T): T {
143+
error.cause ??= options.cause;
144+
145+
return error;
146+
}
147+
}
148+
149+
function makeMsg(
150+
error: ValidationError,
151+
options: { rootName?: string } = {},
152+
): string {
153+
const { rootName: name } = options;
154+
const { instancePath, message } = error;
155+
const pathSection = instancePath.length
156+
? "\n" + new InstancePath(instancePath, { name })
157+
: "";
158+
159+
return message + pathSection;
160+
}
161+
162+
function exposePath(
163+
error: ValidationError,
164+
rootPathDisplay?: string,
165+
): ValidationError {
166+
error.message = makeMsg(error, { rootName: rootPathDisplay });
167+
168+
return error;
169+
}
170+
171+
interface InstancePathOptions {
172+
name?: string;
173+
}
174+
175+
class InstancePath {
176+
paths: readonly PropertyKey[];
177+
name: string;
178+
179+
constructor(
180+
paths: readonly PropertyKey[],
181+
options?: InstancePathOptions,
182+
) {
183+
this.paths = paths;
184+
this.name = options?.name ?? "";
185+
}
186+
187+
toString(): string {
188+
const str = [this.name, ...this.paths]
189+
.filter(Boolean)
190+
.join(".");
191+
192+
return `instance path: ${str}`;
193+
}
194+
}

0 commit comments

Comments
 (0)