Skip to content

Commit

Permalink
Make optional field validation stricter
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelLefkowitz committed Jul 14, 2024
1 parent 7514dc1 commit 40691ef
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 20 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ isPerson({ name: "Joel" }) >>
};
```

Strictly speaking, `{ name: optional(isString) }` implies that the interface is `{ name: string | undefined }` which allows name to be explicitly undefined. Since this is never useful the interface is interpreted as being `{ name?: string }` which is simpler than having a separate function for strictly optional values.

### Using literals

Array literals can be converted directly to validators:
Expand Down
22 changes: 12 additions & 10 deletions src/factories/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ describe("validateWith", () => {
});

expect(validator({ a: "1" }).valid).toBe(true);
expect(validator({ a: "1" }).parsed).toEqual({ a: "1", b: undefined });
expect(validator({ a: "1" }).parsed).toEqual({ a: "1" });
expect(Object.keys(validator({ a: "1" }).parsed)).toEqual(["a"]);

expect(validator({ a: "1", b: undefined }).valid).toBe(true);
expect(validator({ a: "1", b: undefined }).parsed).toEqual({
a: "1",
b: undefined,
});
expect(validator({ a: "1", b: undefined }).parsed).toEqual({ a: "1" });
expect(Object.keys(validator({ a: "1", b: undefined }).parsed)).toEqual([
"a",
]);

expect(validator({ a: "1", b: 1 }).error).toEqual({ b: "Not a string: 1" });
expect(validator({ a: "1", b: "1" }).parsed).toEqual({ a: "1", b: 1 });
Expand Down Expand Up @@ -144,13 +145,14 @@ describe("validateWithAtLeast", () => {
});

expect(validator({ a: "1" }).valid).toBe(true);
expect(validator({ a: "1" }).parsed).toEqual({ a: "1", b: undefined });
expect(validator({ a: "1" }).parsed).toEqual({ a: "1" });
expect(Object.keys(validator({ a: "1" }).parsed)).toEqual(["a"]);

expect(validator({ a: "1", b: undefined }).valid).toBe(true);
expect(validator({ a: "1", b: undefined }).parsed).toEqual({
a: "1",
b: undefined,
});
expect(validator({ a: "1", b: undefined }).parsed).toEqual({ a: "1" });
expect(Object.keys(validator({ a: "1", b: undefined }).parsed)).toEqual([
"a",
]);

expect(validator({ a: "1", b: 1 }).error).toEqual({ b: "Not a string: 1" });
expect(validator({ a: "1", b: "1" }).parsed).toEqual({ a: "1", b: 1 });
Expand Down
34 changes: 24 additions & 10 deletions src/factories/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Validated, Validator } from "../models/validators";
import { ValidatedFields, ValidatorFields } from "../models/fields";
import { invalidate } from "./invalidate";
import { isRecord } from "../validators/records";
import { isUndefined } from "../validators/primitives";
import { merge } from "./results";

/**
Expand Down Expand Up @@ -62,8 +63,11 @@ export const validateWith =
return record as Validated<T>;
}

const isOptional = <T>(validator: Validator<T>) =>
validator(undefined).valid;

const missing = Object.keys(validators).filter(
(i) => !(i in record.parsed || validators[i as keyof T](undefined).valid),
(i) => !(i in record.parsed || isOptional(validators[i as keyof T])),
);

if (missing.length > 0) {
Expand All @@ -79,10 +83,11 @@ export const validateWith =
return invalidate(input, `Unexpected extra fields: ${extra.join(", ")}`);
}

const validated = Object.entries(record.parsed).map(([name, field]) => [
name,
validators[name as keyof T](field),
]);
const validated = Object.entries(record.parsed).reduce<[string, unknown][]>(
(acc, [k, v]) =>
isUndefined(v).valid ? acc : [...acc, [k, validators[k as keyof T](v)]],
[],
);

return merge(Object.fromEntries(validated) as ValidatedFields<T>);
};
Expand Down Expand Up @@ -110,8 +115,11 @@ export const validateWithAtLeast =
return record as Validated<T>;
}

const isOptional = <T>(validator: Validator<T>) =>
validator(undefined).valid;

const missing = Object.keys(validators).filter(
(i) => !(i in record.parsed || validators[i as keyof T](undefined).valid),
(i) => !(i in record.parsed || isOptional(validators[i as keyof T])),
);

if (missing.length > 0) {
Expand All @@ -121,10 +129,16 @@ export const validateWithAtLeast =
);
}

const validated = Object.entries(record.parsed).map(([name, field]) => [
name,
(name in validators ? validators[name as keyof T] : validate)(field),
]);
const validated = Object.entries(record.parsed).reduce<[string, unknown][]>(
(acc, [k, v]) =>
isUndefined(v).valid
? acc
: [
...acc,
[k, (k in validators ? validators[k as keyof T] : validate)(v)],
],
[],
);

return merge(Object.fromEntries(validated) as ValidatedFields<T>);
};

0 comments on commit 40691ef

Please sign in to comment.