Skip to content

Commit

Permalink
breaking: Update and/or/xor to use arrays.
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj committed Oct 2, 2021
1 parent 21474b6 commit 6f9e12d
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 47 deletions.
18 changes: 1 addition & 17 deletions optimal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## 5.0.0-alpha.5 - 2021-10-01

#### 🚀 Updates

- Improve error messages. ([a6a7bb8](https://github.com/milesj/optimal/commit/a6a7bb8))
- Support `undefined` as a first-class type/value. (#39) ([c885310](https://github.com/milesj/optimal/commit/c885310)), closes [#39](https://github.com/milesj/optimal/issues/39)

#### 🐞 Fixes

- Only collection errors for shapes/unions. ([49c64d8](https://github.com/milesj/optimal/commit/49c64d8))

**Note:** Version bump only for package optimal





# 5.0.0

Ground-up rewrite that migrates to a more composable API. Under the hood, classes were refactored
Expand All @@ -39,6 +22,7 @@ changelog will use the new verbiage, but may affect previous APIs.
- Updated `instance()` to no longer accept a schema as an argument, use `instance().of()` instead.
- Updated `object()` to no longer accept a schema as an argument, use `object().of()` instead.
- Updated `union()` to no longer accept a list of schemas as an argument, use `union().of()` instead.
- Updated `.and()`, `.or()`, and `.xor()` to accept a list of keys, instead of an argument spread.
- Renamed `.nonNullable()` method to `.notNullable()`.
- Removed `.key()` method.
- Removed `.message()` method.
Expand Down
27 changes: 18 additions & 9 deletions optimal/src/criteria/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ValidationError } from '../ValidationError';
/**
* Map a list of field names that must be defined alongside this field when in a shape/object.
*/
export function and<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {
export function and<T>(state: SchemaState<T>, keys: string[], options: Options = {}): Criteria<T> {
invariant(keys.length > 0, 'AND requires a list of field names.');

return {
Expand All @@ -35,7 +35,10 @@ export function and<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {
return;
}

invalid(undefs.length === 0, `All of these fields must be defined: ${andKeys.join(', ')}`);
invalid(
undefs.length === 0,
options.message ?? `All of these fields must be defined: ${andKeys.join(', ')}`,
);
},
};
}
Expand Down Expand Up @@ -113,8 +116,6 @@ export function nullable<T>(state: SchemaState<T>) {
* Mark that this field can ONLY use a value that matches the default value.
*/
export function only<T>(state: SchemaState<T>, options: Options = {}): Criteria<T> {
state.metadata.onlyMessage = options.message;

const { defaultValue } = state;

invariant(
Expand All @@ -128,15 +129,20 @@ export function only<T>(state: SchemaState<T>, options: Options = {}): Criteria<
validate(value, path, validateOptions) {
const testValue = extractDefaultValue(defaultValue, path, validateOptions);

invalid(value === testValue, `Value may only be "${testValue}".`, path, value);
invalid(
value === testValue,
options.message ?? `Value may only be "${testValue}".`,
path,
value,
);
},
};
}

/**
* Map a list of field names that must have at least 1 defined when in a shape/object.
*/
export function or<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {
export function or<T>(state: SchemaState<T>, keys: string[], options: Options = {}): Criteria<T> {
invariant(keys.length > 0, 'OR requires a list of field names.');

return {
Expand All @@ -150,7 +156,7 @@ export function or<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {

invalid(
defs.length > 0,
`At least one of these fields must be defined: ${orKeys.join(', ')}`,
options.message ?? `At least one of these fields must be defined: ${orKeys.join(', ')}`,
);
},
};
Expand Down Expand Up @@ -214,7 +220,7 @@ export function when<T>(
/**
* Map a list of field names that must not be defined alongside this field when in a shape/object.
*/
export function xor<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {
export function xor<T>(state: SchemaState<T>, keys: string[], options: Options = {}): Criteria<T> {
invariant(keys.length > 0, 'XOR requires a list of field names.');

return {
Expand All @@ -226,7 +232,10 @@ export function xor<T>(state: SchemaState<T>, ...keys: string[]): Criteria<T> {
(key) => currentObject?.[key] !== undefined && currentObject?.[key] !== null,
);

invalid(defs.length === 1, `Only one of these fields may be defined: ${xorKeys.join(', ')}`);
invalid(
defs.length === 1,
options.message ?? `Only one of these fields may be defined: ${xorKeys.join(', ')}`,
);
},
};
}
6 changes: 3 additions & 3 deletions optimal/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ export type CriteriaFactory<Input> = (
) => Criteria<Input> | void;

export interface CommonCriterias<S> {
and: (...keys: string[]) => S;
and: (keys: string[], options?: Options) => S;
custom: (callback: CriteriaValidator<InferSchemaType<S>>) => S;
deprecate: (message: string) => S;
only: (options?: Options) => S;
optional: () => S;
or: (...keys: string[]) => S;
or: (keys: string[], options?: Options) => S;
required: (options?: Options) => S;
when: (condition: WhenCondition<InferSchemaType<S>>, pass: AnySchema, fail?: AnySchema) => S;
xor: (...keys: string[]) => S;
xor: (keys: string[], options?: Options) => S;
// Define in schemas directly
// never: () => S;
// notNullable: () => S;
Expand Down
24 changes: 12 additions & 12 deletions optimal/tests/optimal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,17 +344,17 @@ describe('Optimal', () => {
describe('logical operators', () => {
it('handles AND', () => {
const and = {
foo: string('a').and('bar', 'baz'),
bar: string('b').and('foo', 'baz'),
baz: string('c').and('foo', 'bar'),
foo: string('a').and(['bar', 'baz']),
bar: string('b').and(['foo', 'baz']),
baz: string('c').and(['foo', 'bar']),
};

// Dont error if all are undefined
expect(() => {
optimal({
foo: string('a').and('bar', 'baz'),
bar: string('b').and('foo', 'baz'),
baz: string('c').and('foo', 'bar'),
foo: string('a').and(['bar', 'baz']),
bar: string('b').and(['foo', 'baz']),
baz: string('c').and(['foo', 'bar']),
}).validate({});
}).not.toThrow();

Expand Down Expand Up @@ -398,9 +398,9 @@ describe('Optimal', () => {

it('handles OR', () => {
const or = {
foo: string('a').or('bar', 'baz'),
bar: string('b').or('foo', 'baz'),
baz: string('c').or('foo', 'bar'),
foo: string('a').or(['bar', 'baz']),
bar: string('b').or(['foo', 'baz']),
baz: string('c').or(['foo', 'bar']),
};

expect(() => {
Expand Down Expand Up @@ -439,9 +439,9 @@ describe('Optimal', () => {

it('handles XOR', () => {
const xor = {
foo: string('a').xor('bar', 'baz'),
bar: string('b').xor('foo', 'baz'),
baz: string('c').xor('foo', 'bar'),
foo: string('a').xor(['bar', 'baz']),
bar: string('b').xor(['foo', 'baz']),
baz: string('c').xor(['foo', 'bar']),
};

expect(() => {
Expand Down
35 changes: 29 additions & 6 deletions optimal/tests/schemas/runCommonTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ export function runCommonTests<T>(
let andSchema: Schema<T>;

beforeEach(() => {
andSchema = schema.and('a', 'c');
andSchema = schema.and(['a', 'c']);
});

it('errors if no keys are defined', () => {
expect(() => {
schema.and();
schema.and([]);
}).toThrow('AND requires a list of field names.');
});

Expand All @@ -114,6 +114,17 @@ export function runCommonTests<T>(
}).toThrow('All of these fields must be defined: a, c');
});

it('can customize the message', () => {
expect(() => {
schema.and(['a', 'c'], { message: 'Missing a/c!' }).validate(value, 'a', {
currentObject: {
a: 'a',
b: 'b',
},
});
}).toThrow('Missing a/c!');
});

if (nullByDefault) {
it('errors if not all properties are defined and null is passed', () => {
expect(() => {
Expand Down Expand Up @@ -407,12 +418,12 @@ export function runCommonTests<T>(
let orSchema: Schema<T>;

beforeEach(() => {
orSchema = schema.or('a', 'b');
orSchema = schema.or(['a', 'b']);
});

it('errors if no keys are defined', () => {
expect(() => {
schema.or();
schema.or([]);
}).toThrow('OR requires a list of field names.');
});

Expand All @@ -422,6 +433,12 @@ export function runCommonTests<T>(
}).toThrow('At least one of these fields must be defined: a, b');
});

it('can customize the message', () => {
expect(() => {
schema.or(['a', 'b'], { message: 'Missing any!' }).validate(value, 'a', {});
}).toThrow('Missing any!');
});

it('doesnt error if at least 1 option is defined', () => {
expect(() => {
orSchema.validate(value, 'a', { currentObject: { a: 'a' } });
Expand All @@ -439,12 +456,12 @@ export function runCommonTests<T>(
let xorSchema: Schema<T>;

beforeEach(() => {
xorSchema = schema.xor('b', 'c');
xorSchema = schema.xor(['b', 'c']);
});

it('errors if no keys are defined', () => {
expect(() => {
schema.xor();
schema.xor([]);
}).toThrow('XOR requires a list of field names.');
});

Expand All @@ -454,6 +471,12 @@ export function runCommonTests<T>(
}).toThrow('Only one of these fields may be defined: a, b, c');
});

it('can customize the message', () => {
expect(() => {
schema.xor(['b', 'c'], { message: 'Only one!' }).validate(value, 'a', {});
}).toThrow('Only one!');
});

it('errors if more than 1 option is defined', () => {
expect(() => {
xorSchema.validate(value, 'a', { currentObject: { a: 'a', b: 'b' } });
Expand Down

0 comments on commit 6f9e12d

Please sign in to comment.