Skip to content

Commit ec0b8ae

Browse files
committed
Allow nested checking
1 parent 36ef889 commit ec0b8ae

3 files changed

Lines changed: 145 additions & 52 deletions

File tree

lib/entry-points.js

Lines changed: 55 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/json/index.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,24 @@ test("validateSchema - validates objects", async (t) => {
103103
t.false(json.validateSchema(objectSchema, { objectKey: 123 }));
104104
});
105105

106+
const checkSchemaTestSchema = {
107+
rootKey: json.object(objectSchema),
108+
};
109+
106110
test("validateSchema - checkSchema reports unknown keys", async (t) => {
107-
const result = json.checkSchema(testSchema, {
108-
requiredKey: "foo",
111+
const result = json.checkSchema(checkSchemaTestSchema, {
112+
rootKey: {
113+
objectKey: {
114+
arrayKey: [],
115+
},
116+
nestedExtraKey: "foo",
117+
},
109118
extraKey: "bar",
110119
});
111120

112121
t.true(result.valid);
113-
t.deepEqual(result.unknownKeys, ["extraKey"]);
122+
t.deepEqual(
123+
result.unknownKeys.sort(),
124+
["extraKey", "rootKey.nestedExtraKey"].sort(),
125+
);
114126
});

src/json/index.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,65 @@ export function isStringOrUndefined(
4848
*/
4949
export type Validator<T> = {
5050
validate: (val: unknown) => val is T;
51+
check: (val: unknown, path: string) => CheckSchemaResult;
5152
required: boolean;
5253
};
5354

55+
function defaultCheck(
56+
validate: (val: unknown) => val is any,
57+
): (arg: unknown) => CheckSchemaResult {
58+
return (arg) => ({ unknownKeys: [], valid: validate(arg) });
59+
}
60+
61+
function makeValidator<T>(
62+
validate: (arg: unknown) => arg is T,
63+
required: boolean = true,
64+
) {
65+
return {
66+
validate,
67+
check: defaultCheck(validate),
68+
required,
69+
} as const satisfies Validator<T>;
70+
}
71+
5472
/** Extracts `T` from `Validator<T>`. */
5573
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;
5674

5775
/** A validator for string fields in schemas. */
58-
export const string = {
59-
validate: isString,
60-
required: true,
61-
} as const satisfies Validator<string>;
76+
export const string = makeValidator(isString);
6277

6378
/** A validator for number fields in schemas. */
64-
export const number = {
65-
validate: isNumber,
66-
required: true,
67-
} as const satisfies Validator<number>;
79+
export const number = makeValidator(isNumber);
6880

6981
/** A validator for arrays. */
7082
export function array<T>(validator: Validator<T>) {
83+
const validate = (val: unknown) => {
84+
return isArray(val) && val.every((e) => validator.validate(e));
85+
};
7186
return {
72-
validate: (val: unknown) => {
73-
return isArray(val) && val.every((e) => validator.validate(e));
87+
validate,
88+
check: (val: unknown, path: string) => {
89+
const result: CheckSchemaResult = { valid: true, unknownKeys: [] };
90+
91+
if (!isArray(val)) {
92+
result.valid = false;
93+
return result;
94+
}
95+
96+
let index = 0;
97+
for (const e of val) {
98+
const eResult = validator.check(e, `${path}[${index}].`);
99+
100+
result.unknownKeys.push(...eResult.unknownKeys);
101+
index++;
102+
103+
if (!eResult.valid) {
104+
result.valid = false;
105+
continue;
106+
}
107+
}
108+
109+
return result;
74110
},
75111
required: true,
76112
} as const satisfies Validator<T[]>;
@@ -85,6 +121,12 @@ export function object<
85121
validate: (val: unknown) => {
86122
return isObject(val) && validateSchema<S, T>(schema, val);
87123
},
124+
check: (val, path) => {
125+
if (!isObject(val)) {
126+
return { valid: false, unknownKeys: [] };
127+
}
128+
return checkSchema(schema, val, {}, path);
129+
},
88130
required: true,
89131
} as const satisfies Validator<T>;
90132
}
@@ -98,6 +140,12 @@ export function optionalOrNull<T>(validator: Validator<T>) {
98140
validate: (val: unknown) => {
99141
return val === undefined || val === null || validator.validate(val);
100142
},
143+
check: (val, path) => {
144+
if (val === undefined || val === null) {
145+
return { valid: true, unknownKeys: [] };
146+
}
147+
return validator.check(val, path);
148+
},
101149
required: false,
102150
} as const satisfies Validator<T | undefined | null>;
103151
}
@@ -111,6 +159,12 @@ export function optional<T>(validator: Validator<T>) {
111159
validate: (val: unknown): val is T | undefined => {
112160
return val === undefined || validator.validate(val);
113161
},
162+
check: (val, path) => {
163+
if (val === undefined) {
164+
return { valid: true, unknownKeys: [] };
165+
}
166+
return validator.check(val, path);
167+
},
114168
required: false,
115169
} as const satisfies Validator<T | undefined>;
116170
}
@@ -193,13 +247,19 @@ export function checkSchema<S extends Schema>(
193247
}
194248

195249
// If the property is present, validate it.
196-
if (hasKey && !validator.validate(obj[key])) {
197-
result.valid = false;
250+
if (hasKey) {
251+
const checkResult = validator.check(obj[key], `${path}${key}.`);
198252

199-
if (options.failFast) {
200-
return result;
253+
result.unknownKeys.push(...checkResult.unknownKeys);
254+
255+
if (!checkResult.valid) {
256+
result.valid = false;
257+
258+
if (options.failFast) {
259+
return result;
260+
}
261+
continue;
201262
}
202-
continue;
203263
}
204264

205265
// If we reach this point, the key has been successfully validated.

0 commit comments

Comments
 (0)