Skip to content

Commit

Permalink
Add optional fields support
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelLefkowitz committed Jul 14, 2024
1 parent 5da2f7c commit 285e7ff
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 4 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ A validation library for TypeScript needs to be:
- Common validators are available
- Lightweight
- Dependency free
- Tiny bundle size (~ 10Kb)
- Tiny bundle size
- Fully tree shakeable

`reviewed` exposes a flexible interface that achieves these goals.
Expand Down Expand Up @@ -245,6 +245,30 @@ if (guard(isNumber)(input)) {
}
```

### Optional fields

Validators can be made optional:

```ts
interface Person {
name?: string;
}

const isPerson = isRecordOf<Person>({ name: optional(isString) });

isPerson({}) >>
{
valid: true,
parsed: { name: "Joel" },
};

isPerson({ name: "Joel" }) >>
{
valid: true,
parsed: { name: "Joel" },
};
```

### Using literals

Array literals can be converted directly to validators:
Expand Down Expand Up @@ -327,6 +351,8 @@ const not: <T>(validator: Validator<T>, reason?: string) => Validator<T>;
const both: <T, U>(first: Validator<T>, second: Validator<U>) => Validator<T & U>;
const either: <T, U>(first: Validator<T>, second: Validator<U>) => Validator<T | U>;

const optional = <T>(validator: Validator<T>): Validator<T | undefined>;

const isArrayOf: <T>(validator: Validator<T>) => Validator<T[]>;
const isRecordOf: <T>(validators: ValidatorFields<T>) => Validator<T>;
```
Expand Down
15 changes: 14 additions & 1 deletion src/factories/transform.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { both, either, not } from "./transform";
import { both, either, not, optional } from "./transform";
import { isNaturalNumberString } from "../validators/strings";
import { isNonEmptyArray, isStringArray } from "../validators/arrays";
import { isNull, isString } from "../validators/primitives";

Expand Down Expand Up @@ -52,3 +53,15 @@ describe("either", () => {
expect(isStringOrNull(1).error).toBe("Not a string: 1");
});
});

describe("optional", () => {
it("allows a validator to accept undefined inputs", () => {
expect(optional(isNaturalNumberString)(1).valid).toBe(false);

expect(optional(isNaturalNumberString)("1").valid).toBe(true);
expect(optional(isNaturalNumberString)("1").parsed).toBe(1);

expect(optional(isNaturalNumberString)(undefined).valid).toBe(true);
expect(optional(isNaturalNumberString)(undefined).parsed).toBe(undefined);
});
});
30 changes: 30 additions & 0 deletions src/factories/transform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Validated, Validator } from "../models/validators";
import { invalidateWith } from "./invalidate";
import { isUndefined } from "../validators/primitives";
import { validate } from "./validate";

/**
Expand Down Expand Up @@ -114,3 +115,32 @@ export const either =

return right.valid ? right : left;
};

/**
* Allow a validator to accept undefined inputs
*
* @category Factories
* @example
* interface Person {
* name?: string;
* }
*
* const isPerson = isRecordOf<Person>({ name: optional(isString) });
*
* isPerson({}) >>
* {
* valid: true,
* parsed: { name: undefined },
* };
*
* isPerson({ name: "Joel" }) >>
* {
* valid: true,
* parsed: { name: "Joel" },
* };
*
* @typeParam T - The validated type
* @param first - The validator
*/
export const optional = <T>(validator: Validator<T>): Validator<T | undefined> =>
either(validator, isUndefined);
34 changes: 34 additions & 0 deletions src/factories/validate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { isNaturalNumberString } from "../validators/strings";
import { isNumber, isString } from "../validators/primitives";
import { optional } from "./transform";
import { validate, validateWith, validateWithAtLeast } from "./validate";

describe("validate", () => {
Expand Down Expand Up @@ -62,6 +64,22 @@ describe("validateWith", () => {
error: "Unexpected extra fields: c",
});
});

it("parses optional fields", () => {
const validator = validateWith<{ a: string; b?: number }>({
a: isString,
b: optional(isNaturalNumberString),
});

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

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: 1 }).error).toEqual({ b: "Not a string: 1" });
expect(validator({ a: "1", b: "1" }).parsed).toEqual({ a: "1", b: 1 });
});
});

describe("validateWithAtLeast", () => {
Expand Down Expand Up @@ -115,4 +133,20 @@ describe("validateWithAtLeast", () => {
error: null,
});
});

it("parses optional fields", () => {
const validator = validateWithAtLeast<{ a: string; b?: number }>({
a: isString,
b: optional(isNaturalNumberString),
});

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

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: 1 }).error).toEqual({ b: "Not a string: 1" });
expect(validator({ a: "1", b: "1" }).parsed).toEqual({ a: "1", b: 1 });
});
});
4 changes: 2 additions & 2 deletions src/factories/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const validateWith =
}

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

if (missing.length > 0) {
Expand Down Expand Up @@ -111,7 +111,7 @@ export const validateWithAtLeast =
}

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

if (missing.length > 0) {
Expand Down

0 comments on commit 285e7ff

Please sign in to comment.