Skip to content

Commit

Permalink
Add tuple
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Jul 30, 2024
1 parent a4db3c0 commit 85e4825
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"size-limit": [
{
"path": "./dist/contracts.js",
"limit": "749 B"
"limit": "797 B"
}
]
}
65 changes: 64 additions & 1 deletion packages/contracts/src/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { describe, it, test, expect } from 'vitest';

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

describe('bool', () => {
it('valid', () => {
Expand Down Expand Up @@ -484,3 +495,55 @@ describe('complex nested', () => {
`);
});
});

describe('tuple', () => {
it('one element', () => {
const cntrct = tuple(str);

expect(cntrct.isData(['a'])).toBeTruthy();
expect(cntrct.getErrorMessages(['a'])).toEqual([]);

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

it('two elements', () => {
const cntrct = tuple(str, num);

expect(cntrct.isData(['a', 1])).toBeTruthy();
expect(cntrct.getErrorMessages(['a', 1])).toEqual([]);

expect(cntrct.isData(['a', 'b'])).toBeFalsy();
expect(cntrct.getErrorMessages(['a', 'b'])).toMatchInlineSnapshot(`
[
"1: expected number, got string",
]
`);

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

it('three elements', () => {
const cntrct = tuple(str, num, bool);

expect(cntrct.isData(['a', 1, true])).toBeTruthy();
expect(cntrct.getErrorMessages(['a', 1, true])).toEqual([]);

expect(cntrct.isData(['a', 1, 'b'])).toBeFalsy();
expect(cntrct.getErrorMessages(['a', 1, 'b'])).toMatchInlineSnapshot(`
[
"2: expected boolean, got string",
]
`);
});
});
81 changes: 81 additions & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,87 @@ export function arr<V>(c: Contract<unknown, V>): Contract<unknown, V[]> {
};
}

export function tuple<T>(a: Contract<unknown, T>): Contract<unknown, [T]>;
export function tuple<T, U>(
a: Contract<unknown, T>,
b: Contract<unknown, U>
): Contract<unknown, [T, U]>;
export function tuple<T, U, V>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>
): Contract<unknown, [T, U, V]>;
export function tuple<T, U, V, W>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>,
d: Contract<unknown, W>
): Contract<unknown, [T, U, V, W]>;
export function tuple<T, U, V, W, X>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>,
d: Contract<unknown, W>,
e: Contract<unknown, X>
): Contract<unknown, [T, U, V, W, X]>;
export function tuple<T, U, V, W, X, Y>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>,
d: Contract<unknown, W>,
e: Contract<unknown, X>,
f: Contract<unknown, Y>
): Contract<unknown, [T, U, V, W, X, Y]>;
export function tuple<T, U, V, W, X, Y, Z>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>,
d: Contract<unknown, W>,
e: Contract<unknown, X>,
f: Contract<unknown, Y>,
g: Contract<unknown, Z>
): Contract<unknown, [T, U, V, W, X, Y, Z]>;
export function tuple<T, U, V, W, X, Y, Z, A>(
a: Contract<unknown, T>,
b: Contract<unknown, U>,
c: Contract<unknown, V>,
d: Contract<unknown, W>,
e: Contract<unknown, X>,
f: Contract<unknown, Y>,
g: Contract<unknown, Z>,
h: Contract<unknown, A>
): Contract<unknown, [T, U, V, W, X, Y, Z, A]>;
/**
* Function that creates a _Contract_ that checks if a value is conform to a tuple of the given _Contracts_.
*
* @example
* const userAges = tuple(str, num);
*
* userAges.isData(['Alice', 42]) === true;
* userAges.isData(['Alice', 'what']) === false;
*/
export function tuple(...contracts: Array<Contract<unknown, any>>): any {
const check = (x: unknown): x is any[] =>
Array.isArray(x) &&
x.length === contracts.length &&
contracts.every((c, i) => c.isData(x[i]));

return {
isData: check,
getErrorMessages: createGetErrorMessages(check, (x) => {
if (!Array.isArray(x)) {
return [`expected tuple, got ${typeOf(x)}`];
}

return x.flatMap((v, idx) =>
contracts[idx]
.getErrorMessages(v)
.map((message) => `${idx}: ${message}`)
);
}),
};
}

// -- utils

function createSimpleContract<T>(
Expand Down

0 comments on commit 85e4825

Please sign in to comment.