Skip to content

Commit

Permalink
and
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Jul 30, 2024
1 parent df8f944 commit ae7991a
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 7 deletions.
8 changes: 4 additions & 4 deletions apps/website/docs/contracts/cookbook/custom_matchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Let us write a custom matcher that checks if an age of a user is within a certai
```ts
import { type Contract } from '@withease/contracts';

function age(min, max): Contract<unknown, number> {
function age(min, max): Contract<number, number> {
return {
isData: (data) => typeof data === 'number' && data >= min && data <= max,
isData: (data) => data >= min && data <= max,
getErrorMessages: (data) =>
`Expected a number between ${min} and ${max}, but got ${data}`,
};
Expand All @@ -19,10 +19,10 @@ function age(min, max): Contract<unknown, number> {
Now you can use this matcher in your schema:

```ts
import { rec, str } from '@withease/contracts';
import { rec, str, and, num } from '@withease/contracts';

const User = rec({
name: str,
age: age(18, 100),
age: and(num, age(18, 100)),
});
```
43 changes: 42 additions & 1 deletion packages/contracts/src/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, test, expect } from 'vitest';

import { bool, num, str, rec, or, val, arr } from './index';
import { bool, num, str, rec, or, val, arr, and, Contract } from './index';

describe('bool', () => {
it('valid', () => {
Expand Down Expand Up @@ -240,6 +240,47 @@ describe('str', () => {
});
});

describe('and', () => {
const len = (l: number): Contract<string, string> => {
return {
isData: (x: string): x is string => x.length >= l,
getErrorMessages: (x) =>
x.length >= l ? [] : [`expected length >= ${l}, got ${x.length}`],
};
};

const cntrct = and(str, len(10));

it('valid all', () => {
const str10 = '1234567890';

expect(cntrct.isData(str10)).toBeTruthy();
expect(cntrct.getErrorMessages(str10)).toEqual([]);
});

it('invalid first', () => {
const number = 1234567890;

expect(cntrct.isData(number)).toBeFalsy();
expect(cntrct.getErrorMessages(number)).toMatchInlineSnapshot(`
[
"expected string, got number",
]
`);
});

it('invalid second', () => {
const str9 = '123456789';

expect(cntrct.isData(str9)).toBeFalsy();
expect(cntrct.getErrorMessages(str9)).toMatchInlineSnapshot(`
[
"expected length >= 10, got 9",
]
`);
});
});

describe('or', () => {
const cntrct = or(bool, val(0));
it('valid', () => {
Expand Down
42 changes: 40 additions & 2 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { func } from 'superstruct';

/**
* A type that allows to extract the result type of a _Contract_.
*
Expand Down Expand Up @@ -123,12 +125,48 @@ export function or<T extends Array<Contract<unknown, any>>>(
};
}

/**
* Function that creates a _Contract_ that checks if a value is conform to all of the given _Contracts_.
*
* @example
* function age(min, max): Contract<number, number> {
* return {
* isData: (data) => data >= min && data <= max,
* getErrorMessages: (data) =>
* `Expected a number between ${min} and ${max}, but got ${data}`,
* };
* }
*
* const User = rec({
* name: str,
* age: and(num, age(18, 100)),
* });
*/
export function and<T>(
first: Contract<unknown, T>,
...rest: Array<Contract<T, T>>
): Contract<unknown, T> {
const all = [first, ...rest];
return {
isData: (x): x is T => all.every((c) => c.isData(x as any)),
getErrorMessages: (x) => {
for (const c of all) {
if (!c.isData(x as any)) {
return c.getErrorMessages(x as any);
}
}

return [];
},
};
}

/**
* Function that creates a _Contract_ that checks if a value is object and every property is conform to the given _Contract_.
*
*
* @example
* const Ids = rec(str, num);
*
*
* Ids.isData({ id1: 1, id2: 2 }) === true;
* Ids.isData({ id1: 1, id2: '2' }) === false;
*/
Expand Down

0 comments on commit ae7991a

Please sign in to comment.