Skip to content

Commit 38f04f4

Browse files
committed
Add parsing modes
1 parent 2b43eec commit 38f04f4

File tree

2 files changed

+67
-59
lines changed

2 files changed

+67
-59
lines changed

src/index.ts

Lines changed: 66 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,20 @@ function toTerminals(type: Type): TerminalType[] {
129129

130130
type Infer<T extends Type> = T extends Type<infer I> ? I : never;
131131

132-
type Func<T> = (v: unknown) => Result<T>;
132+
const enum FuncMode {
133+
PASS = 0,
134+
STRICT = 1,
135+
STRIP = 2,
136+
}
137+
type Func<T> = (v: unknown, mode: FuncMode) => Result<T>;
138+
139+
type ParseOptions = {
140+
mode: "passthrough" | "strict" | "strip";
141+
};
133142

134143
abstract class Type<Out = unknown> {
135144
abstract readonly name: string;
136-
abstract genFunc(): (v: unknown) => Result<Out>;
145+
abstract genFunc(): Func<Out>;
137146
abstract toTerminals(into: TerminalType[]): void;
138147

139148
get isOptional(): boolean {
@@ -154,8 +163,15 @@ abstract class Type<Out = unknown> {
154163
return f;
155164
}
156165

157-
parse(v: unknown): Out {
158-
const r = this.func(v);
166+
parse(v: unknown, options?: Partial<ParseOptions>): Out {
167+
let mode: FuncMode = FuncMode.PASS;
168+
if (options && options.mode === "strict") {
169+
mode = FuncMode.STRICT;
170+
} else if (options && options.mode === "strip") {
171+
mode = FuncMode.STRIP;
172+
}
173+
174+
const r = this.func(v, mode);
159175
if (r === true) {
160176
return v as Out;
161177
} else if (r.code === "ok") {
@@ -178,44 +194,37 @@ type Optionals<T extends Record<string, Type>> = {
178194
[K in keyof T]: undefined extends Infer<T[K]> ? K : never;
179195
}[keyof T];
180196

181-
type UnknownKeys = "passthrough" | "strict" | "strip" | Type;
182-
183197
type ObjectShape = Record<string, Type>;
184198

185199
type ObjectOutput<
186200
T extends ObjectShape,
187-
U extends UnknownKeys
201+
R extends Type | undefined
188202
> = PrettyIntersection<
189203
{ [K in Optionals<T>]?: Infer<T[K]> } &
190204
{ [K in Exclude<keyof T, Optionals<T>>]: Infer<T[K]> } &
191-
(U extends "passthrough" ? { [K: string]: unknown } : unknown) &
192-
(U extends Type ? { [K: string]: Infer<U> } : unknown)
205+
(R extends Type ? { [K: string]: Infer<R> } : unknown)
193206
>;
194207

195208
class ObjectType<
196209
T extends ObjectShape = ObjectShape,
197-
U extends UnknownKeys = UnknownKeys
198-
> extends Type<ObjectOutput<T, U>> {
210+
Rest extends Type | undefined = Type | undefined
211+
> extends Type<ObjectOutput<T, Rest>> {
199212
readonly name = "object";
200213

201-
constructor(readonly shape: T, private readonly unknownKeys: U) {
214+
constructor(readonly shape: T, private readonly restType: Rest) {
202215
super();
203216
}
204217

205218
toTerminals(into: TerminalType[]): void {
206219
into.push(this);
207220
}
208221

209-
genFunc(): Func<ObjectOutput<T, U>> {
222+
genFunc(): Func<ObjectOutput<T, Rest>> {
210223
const shape = this.shape;
211-
const strip = this.unknownKeys === "strip";
212-
const strict = this.unknownKeys === "strict";
213-
const passthrough = this.unknownKeys === "passthrough";
214-
const catchall =
215-
this.unknownKeys instanceof Type ? this.unknownKeys.func : undefined;
224+
const rest = this.restType ? this.restType.func : undefined;
216225

217226
const keys: string[] = [];
218-
const funcs: ((v: unknown) => Result<unknown>)[] = [];
227+
const funcs: Func<unknown>[] = [];
219228
const required: boolean[] = [];
220229
const knownKeys = Object.create(null);
221230
const shapeTemplate = {} as Record<string, unknown>;
@@ -227,23 +236,27 @@ class ObjectType<
227236
shapeTemplate[key] = undefined;
228237
}
229238

230-
return (obj) => {
239+
return (obj, mode) => {
231240
if (!isObject(obj)) {
232241
return { code: "invalid_type", expected: ["object"] };
233242
}
243+
const pass = mode === FuncMode.PASS;
244+
const strict = mode === FuncMode.STRICT;
245+
const strip = mode === FuncMode.STRIP;
246+
const template = pass || rest ? obj : shapeTemplate;
247+
234248
let issueTree: IssueTree | undefined = undefined;
235249
let output: Record<string, unknown> = obj;
236-
const template = strict || strip ? shapeTemplate : obj;
237-
if (!passthrough) {
250+
if (strict || strip || rest) {
238251
for (const key in obj) {
239252
if (!knownKeys[key]) {
240253
if (strict) {
241254
return { code: "unrecognized_key", key };
242255
} else if (strip) {
243256
output = { ...template };
244257
break;
245-
} else if (catchall) {
246-
const r = catchall(obj[key]);
258+
} else if (rest) {
259+
const r = rest(obj[key], mode);
247260
if (r !== true) {
248261
if (r.code === "ok") {
249262
if (output === obj) {
@@ -266,7 +279,7 @@ class ObjectType<
266279
if (value === undefined && required[i]) {
267280
return { code: "missing_key", key };
268281
} else {
269-
const r = funcs[i](value);
282+
const r = funcs[i](value, mode);
270283
if (r !== true) {
271284
if (r.code === "ok") {
272285
if (output === obj) {
@@ -285,21 +298,12 @@ class ObjectType<
285298
} else if (obj === output) {
286299
return true;
287300
} else {
288-
return { code: "ok", value: output as ObjectOutput<T, U> };
301+
return { code: "ok", value: output as ObjectOutput<T, Rest> };
289302
}
290303
};
291304
}
292-
passthrough(): ObjectType<T, "passthrough"> {
293-
return new ObjectType(this.shape, "passthrough");
294-
}
295-
strict(): ObjectType<T, "strict"> {
296-
return new ObjectType(this.shape, "strict");
297-
}
298-
strip(): ObjectType<T, "strip"> {
299-
return new ObjectType(this.shape, "strip");
300-
}
301-
catchall<C extends Type>(catchall: C): ObjectType<T, C> {
302-
return new ObjectType(this.shape, catchall);
305+
rest<R extends Type>(restType: R): ObjectType<T, R> {
306+
return new ObjectType(this.shape, restType);
303307
}
304308
}
305309

@@ -316,14 +320,14 @@ class ArrayType<T extends Type = Type> extends Type<Infer<T>[]> {
316320

317321
genFunc(): Func<Infer<T>[]> {
318322
const func = this.item.func;
319-
return (arr) => {
323+
return (arr, mode) => {
320324
if (!Array.isArray(arr)) {
321325
return { code: "invalid_type", expected: ["array"] };
322326
}
323327
let issueTree: IssueTree | undefined = undefined;
324328
let output: Infer<T>[] = arr;
325329
for (let i = 0; i < arr.length; i++) {
326-
const r = func(arr[i]);
330+
const r = func(arr[i], mode);
327331
if (r !== true) {
328332
if (r.code === "ok") {
329333
if (output === arr) {
@@ -391,8 +395,12 @@ function createObjectMatchers(
391395
t: { root: Type; terminal: TerminalType }[]
392396
): {
393397
key: string;
394-
matcher: (rootValue: unknown, value: unknown) => Result<unknown>;
395398
isOptional: boolean;
399+
matcher: (
400+
rootValue: unknown,
401+
value: unknown,
402+
mode: FuncMode
403+
) => Result<unknown>;
396404
}[] {
397405
const objects: {
398406
root: Type;
@@ -460,7 +468,7 @@ function createObjectMatchers(
460468

461469
function createUnionMatcher(
462470
t: { root: Type; terminal: TerminalType }[]
463-
): (rootValue: unknown, value: unknown) => Result<unknown> {
471+
): (rootValue: unknown, value: unknown, mode: FuncMode) => Result<unknown> {
464472
const literals = new Map<unknown, Type[]>();
465473
const types = new Map<BaseType, Type[]>();
466474
const allTypes = new Set<BaseType>();
@@ -504,7 +512,7 @@ function createUnionMatcher(
504512
expected: expectedLiterals,
505513
};
506514

507-
return (rootValue: unknown, value: unknown) => {
515+
return (rootValue, value, mode) => {
508516
const type = toBaseType(value);
509517
if (!allTypes.has(type)) {
510518
return invalidType;
@@ -514,7 +522,7 @@ function createUnionMatcher(
514522
if (options) {
515523
let issueTree: IssueTree | undefined;
516524
for (let i = 0; i < options.length; i++) {
517-
const r = options[i].func(rootValue);
525+
const r = options[i].func(rootValue, mode);
518526
if (r === true || r.code === "ok") {
519527
return r;
520528
}
@@ -560,20 +568,20 @@ class UnionType<T extends Type[] = Type[]> extends Type<Infer<T[number]>> {
560568
);
561569
const objects = createObjectMatchers(flattened);
562570
const base = createUnionMatcher(flattened);
563-
return (v) => {
571+
return (v, mode) => {
564572
if (objects.length > 0 && isObject(v)) {
565573
const item = objects[0];
566574
const value = v[item.key];
567575
if (value === undefined && !item.isOptional && !(item.key in v)) {
568576
return { code: "missing_key", key: item.key };
569577
}
570-
const r = item.matcher(v, value);
578+
const r = item.matcher(v, value, mode);
571579
if (r === true || r.code === "ok") {
572580
return r as Result<Infer<T[number]>>;
573581
}
574582
return prependPath(item.key, r);
575583
}
576-
return base(v, v) as Result<Infer<T[number]>>;
584+
return base(v, v, mode) as Result<Infer<T[number]>>;
577585
};
578586
}
579587
}
@@ -582,7 +590,7 @@ class NumberType extends Type<number> {
582590
readonly name = "number";
583591
genFunc(): Func<number> {
584592
const issue: Issue = { code: "invalid_type", expected: ["number"] };
585-
return (v) => (typeof v === "number" ? true : issue);
593+
return (v, _mode) => (typeof v === "number" ? true : issue);
586594
}
587595
toTerminals(into: TerminalType[]): void {
588596
into.push(this);
@@ -592,7 +600,7 @@ class StringType extends Type<number> {
592600
readonly name = "string";
593601
genFunc(): Func<number> {
594602
const issue: Issue = { code: "invalid_type", expected: ["string"] };
595-
return (v) => (typeof v === "string" ? true : issue);
603+
return (v, _mode) => (typeof v === "string" ? true : issue);
596604
}
597605
toTerminals(into: TerminalType[]): void {
598606
into.push(this);
@@ -602,7 +610,7 @@ class BigIntType extends Type<number> {
602610
readonly name = "bigint";
603611
genFunc(): Func<number> {
604612
const issue: Issue = { code: "invalid_type", expected: ["bigint"] };
605-
return (v) => (typeof v === "bigint" ? true : issue);
613+
return (v, _mode) => (typeof v === "bigint" ? true : issue);
606614
}
607615
toTerminals(into: TerminalType[]): void {
608616
into.push(this);
@@ -612,7 +620,7 @@ class BooleanType extends Type<number> {
612620
readonly name = "boolean";
613621
genFunc(): Func<number> {
614622
const issue: Issue = { code: "invalid_type", expected: ["boolean"] };
615-
return (v) => (typeof v === "boolean" ? true : issue);
623+
return (v, _mode) => (typeof v === "boolean" ? true : issue);
616624
}
617625
toTerminals(into: TerminalType[]): void {
618626
into.push(this);
@@ -622,7 +630,7 @@ class UndefinedType extends Type<undefined> {
622630
readonly name = "undefined";
623631
genFunc(): Func<undefined> {
624632
const issue: Issue = { code: "invalid_type", expected: ["undefined"] };
625-
return (v) => (v === undefined ? true : issue);
633+
return (v, _mode) => (v === undefined ? true : issue);
626634
}
627635
toTerminals(into: TerminalType[]): void {
628636
into.push(this);
@@ -632,7 +640,7 @@ class NullType extends Type<null> {
632640
readonly name = "null";
633641
genFunc(): Func<null> {
634642
const issue: Issue = { code: "invalid_type", expected: ["null"] };
635-
return (v) => (v === null ? true : issue);
643+
return (v, _mode) => (v === null ? true : issue);
636644
}
637645
toTerminals(into: TerminalType[]): void {
638646
into.push(this);
@@ -646,7 +654,7 @@ class LiteralType<Out extends Literal = Literal> extends Type<Out> {
646654
genFunc(): Func<Out> {
647655
const value = this.value;
648656
const issue: Issue = { code: "invalid_literal", expected: [value] };
649-
return (v) => (v === value ? true : issue);
657+
return (v, _) => (v === value ? true : issue);
650658
}
651659
toTerminals(into: TerminalType[]): void {
652660
into.push(this);
@@ -666,7 +674,7 @@ class OptionalType<Out, Default> extends Type<Out | Default> {
666674
this.defaultValue === undefined
667675
? true
668676
: ({ code: "ok", value: this.defaultValue } as const);
669-
return (v) => (v === undefined ? defaultResult : func(v));
677+
return (v, mode) => (v === undefined ? defaultResult : func(v, mode));
670678
}
671679
toTerminals(into: TerminalType[]): void {
672680
into.push(undefined_());
@@ -684,8 +692,8 @@ class TransformType<Out> extends Type<Out> {
684692
genFunc(): Func<Out> {
685693
const f = this.transformed.func;
686694
const t = this.transformFunc;
687-
return (v) => {
688-
const r = f(v);
695+
return (v, mode) => {
696+
const r = f(v, mode);
689697
if (r !== true && r.code !== "ok") {
690698
return r;
691699
}
@@ -717,8 +725,8 @@ function null_(): NullType {
717725
}
718726
function object<T extends Record<string, Type>>(
719727
obj: T
720-
): ObjectType<T, "strict"> {
721-
return new ObjectType(obj, "strict");
728+
): ObjectType<T, undefined> {
729+
return new ObjectType(obj, undefined);
722730
}
723731
function array<T extends Type>(item: T): ArrayType<T> {
724732
return new ArrayType(item);

tests/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe("object()", () => {
7777
expect(t.parse(o)).to.not.equal(o);
7878
});
7979
it("rejects other types", () => {
80-
const t = v.object({}).passthrough();
80+
const t = v.object({});
8181
for (const val of ["1", 1n, true, null, undefined, []]) {
8282
expect(() => t.parse(val)).to.throw(v.ValitaError);
8383
}

0 commit comments

Comments
 (0)