Skip to content

Commit

Permalink
Cover some edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Jul 30, 2024
1 parent a942eef commit 4154d8a
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 1 deletion.
4 changes: 4 additions & 0 deletions apps/website/docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ export default defineConfig({
text: 'Custom Matchers',
link: '/contracts/cookbook/custom_matchers',
},
{
text: 'Merge Objects',
link: '/contracts/cookbook/merge_objects',
},
],
},
{ text: 'APIs', link: '/contracts/api' },
Expand Down
24 changes: 24 additions & 0 deletions apps/website/docs/contracts/cookbook/merge_objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Merge Objects

Merge two [_Contracts_](/primitives/contract) representing objects into a single [_Contract_](/primitives/contract) representing an object with fields from both input objects is a common operation in many applications.

With `@withease/contracts` in can be done with simple `and` call:

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

const Price = obj({
currency: str,
value: num,
});

const PriceWithDiscount = and(
Price,
obj({
discount: num,
})
);

type TPriceWithDiscount = UnContract<typeof PriceWithDiscount>;
// 👆 { currency: string, value: number, discount: number }
```
10 changes: 9 additions & 1 deletion packages/contracts/src/contract.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, expectTypeOf } from 'vitest';

import { and, Contract, num } from './index';
import { and, Contract, num, obj, str } from './index';

describe('and', () => {
test('inline contract', () => {
Expand All @@ -11,4 +11,12 @@ describe('and', () => {

expectTypeOf(contract).toEqualTypeOf<Contract<unknown, number>>();
});

test('as extends', () => {
const contract = and(obj({ name: str }), obj({ age: num }));

expectTypeOf(contract).toEqualTypeOf<
Contract<unknown, { name: string; age: number }>
>();
});
});
24 changes: 24 additions & 0 deletions packages/contracts/src/contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,27 @@ describe('tuple', () => {
`);
});
});

describe('special cases', () => {
it('or with two big objects', () => {
const cntrct = or(obj({ name: str }), obj({ age: num }));

expect(cntrct.getErrorMessages({ lol: 'kek' })).toMatchInlineSnapshot(`
[
"name: expected string, got undefined",
"age: expected number, got undefined",
]
`);
});

it('and as extends', () => {
const contract = and(obj({ name: str }), obj({ age: num }));

expect(contract.isData({ name: 'a', age: 1 })).toBeTruthy();

expect(contract.isData({ name: 'a' })).toBeFalsy();
expect(contract.isData({ age: 1 })).toBeFalsy();
expect(contract.isData({ name: 'a', age: 'ERROR' })).toBeFalsy();
expect(contract.isData({ name: 18888, age: 1 })).toBeFalsy();
});
});
38 changes: 38 additions & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,42 @@ export function or<T extends Array<Contract<unknown, any>>>(
};
}

type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof U
? U[K]
: K extends keyof T
? T[K]
: never;
};

/**
* Function that merges two _Contracts_ of objects into one.
*
* @overload "and(objectA, objectB)"
*
* @example
*
* const User = obj({
* name: str,
* });
*
* const Admin = and(User, obj({
* permitted: bool,
* }));
*/
export function and<
T extends Record<string, any>,
K extends Record<string, any>
>(
first: Contract<unknown, T>,
second: Contract<unknown, K>
): Contract<unknown, Merge<T, K>>;

/**
* Function that creates a _Contract_ that checks if a value is conform to all of the given _Contracts_.
*
* @overload "and(first, ...rest)"
*
* @example
* function age(min, max): Contract<number, number> {
* return {
Expand All @@ -125,6 +158,11 @@ export function or<T extends Array<Contract<unknown, any>>>(
* age: and(num, age(18, 100)),
* });
*/
export function and<T>(
first: Contract<unknown, T>,
...rest: Array<Contract<T, T>>
): Contract<unknown, T>;

export function and<T>(
first: Contract<unknown, T>,
...rest: Array<Contract<T, T>>
Expand Down

0 comments on commit 4154d8a

Please sign in to comment.