Skip to content

Commit

Permalink
Merge b01903b into 98dcde0
Browse files Browse the repository at this point in the history
  • Loading branch information
ForbesLindesay committed Sep 6, 2020
2 parents 98dcde0 + b01903b commit e222f4a
Show file tree
Hide file tree
Showing 36 changed files with 1,303 additions and 898 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"jest": "24.9.0",
"prettier": "^1.19.1",
"ts-jest": "^24.1.0",
"type-assertions": "^1.1.0",
"typescript": "4.0.2"
},
"keywords:": [
Expand Down
54 changes: 38 additions & 16 deletions src/asynccontract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
import { AsyncContract, Number } from '.';
import { ValidationError } from './errors';

describe('AsyncContract', () => {
describe('when function does not return a promise', () => {
it('throws a validation error', () => {
const contractedFunction = AsyncContract(Number).enforce(() => 7 as any);
expect(contractedFunction).toThrow(ValidationError);
it('throws a validation error', async () => {
const contractedFunction = AsyncContract([], Number).enforce(() => 7 as any);
await expect(contractedFunction()).rejects.toMatchInlineSnapshot(
`[ValidationError: Expected function to return a promise, but instead got 7]`,
);
});
});
describe('when a function does return a promise, but for the wrong type', () => {
it('throws a validation error asynchronously', async () => {
const contractedFunction = AsyncContract(Number).enforce(() => Promise.resolve('hi' as any));
try {
await contractedFunction();
fail();
} catch (e) {
expect(e).toBeInstanceOf(ValidationError);
}
const contractedFunction = AsyncContract([], Number).enforce(() =>
Promise.resolve('hi' as any),
);
await expect(contractedFunction()).rejects.toMatchInlineSnapshot(
`[ValidationError: Expected number, but was string]`,
);
});
});
describe('when a function does return a promise', () => {
describe('when a function does return a promise, for the correct type', () => {
it('should validate successfully', async () => {
const contractedFunction = AsyncContract(Number).enforce(() => Promise.resolve(7));
const contractedFunction = AsyncContract([], Number).enforce(() => Promise.resolve(7));
await expect(contractedFunction()).resolves.toBe(7);
});
});
describe('when not enough arguments are provided', () => {
it('throws a validation error', () => {
const contractedFunction = AsyncContract(Number, Number).enforce(n => Promise.resolve(n + 1));
expect(contractedFunction).toThrow(ValidationError);
it('throws a validation error', async () => {
const contractedFunction = AsyncContract([Number], Number).enforce(n =>
Promise.resolve(n + 1),
);
await expect((contractedFunction as any)()).rejects.toMatchInlineSnapshot(
`[ValidationError: Expected 1 arguments but only received 0]`,
);
});
});
describe('when arguments are of the wrong type', () => {
it('throws a validation error', async () => {
const contractedFunction = AsyncContract([Number], Number).enforce(n =>
Promise.resolve(n + 1),
);
await expect(contractedFunction('whatever' as any)).rejects.toMatchInlineSnapshot(
`[ValidationError: Expected number, but was string]`,
);
});
});
describe('when arguments are valid', () => {
it('throws a validation error', async () => {
const contractedFunction = AsyncContract([Number], Number).enforce(n =>
Promise.resolve(n + 1),
);
await expect(contractedFunction(41)).resolves.toEqual(42);
});
});
});
185 changes: 35 additions & 150 deletions src/asynccontract.ts
Original file line number Diff line number Diff line change
@@ -1,165 +1,50 @@
import { Runtype } from './index';
import { ValidationError } from './errors';
import { RuntypeBase } from './runtype';

export interface AsyncContract0<Z> {
enforce(f: () => Promise<Z>): () => Promise<Z>;
}

export interface AsyncContract1<A, Z> {
enforce(f: (a: A) => Promise<Z>): (a: A) => Promise<Z>;
}

export interface AsyncContract2<A, B, Z> {
enforce(f: (a: A, b: B) => Promise<Z>): (a: A, b: B) => Promise<Z>;
}

export interface AsyncContract3<A, B, C, Z> {
enforce(f: (a: A, b: B, c: C) => Promise<Z>): (a: A, b: B, c: C) => Promise<Z>;
}

export interface AsyncContract4<A, B, C, D, Z> {
enforce(f: (a: A, b: B, c: C, d: D) => Promise<Z>): (a: A, b: B, c: C, d: D) => Promise<Z>;
}

export interface AsyncContract5<A, B, C, D, E, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E) => Promise<Z>;
}

export interface AsyncContract6<A, B, C, D, E, F, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E, f: F) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E, f: F) => Promise<Z>;
}

export interface AsyncContract7<A, B, C, D, E, F, G, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise<Z>;
}

export interface AsyncContract8<A, B, C, D, E, F, G, H, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise<Z>;
}

export interface AsyncContract9<A, B, C, D, E, F, G, H, I, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise<Z>;
}

export interface AsyncContract10<A, B, C, D, E, F, G, H, I, J, Z> {
enforce(
f: (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise<Z>,
): (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise<Z>;
export interface AsyncContract<A extends any[], Z> {
enforce(f: (...a: A) => Promise<Z>): (...a: A) => Promise<Z>;
}

/**
* Create a function contract.
*/
export function AsyncContract<Z>(Z: Runtype<Z>): AsyncContract0<Z>;
export function AsyncContract<A, Z>(A: Runtype<A>, Z: Runtype<Z>): AsyncContract1<A, Z>;
export function AsyncContract<A, B, Z>(
A: Runtype<A>,
B: Runtype<B>,
Z: Runtype<Z>,
): AsyncContract2<A, B, Z>;
export function AsyncContract<A, B, C, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
Z: Runtype<Z>,
): AsyncContract3<A, B, C, Z>;
export function AsyncContract<A, B, C, D, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
Z: Runtype<Z>,
): AsyncContract4<A, B, C, D, Z>;
export function AsyncContract<A, B, C, D, E, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
Z: Runtype<Z>,
): AsyncContract5<A, B, C, D, E, Z>;
export function AsyncContract<A, B, C, D, E, F, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
F: Runtype<F>,
Z: Runtype<Z>,
): AsyncContract6<A, B, C, D, E, F, Z>;
export function AsyncContract<A, B, C, D, E, F, G, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
F: Runtype<F>,
G: Runtype<G>,
Z: Runtype<Z>,
): AsyncContract7<A, B, C, D, E, F, G, Z>;
export function AsyncContract<A, B, C, D, E, F, G, H, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
F: Runtype<F>,
G: Runtype<G>,
H: Runtype<H>,
Z: Runtype<Z>,
): AsyncContract8<A, B, C, D, E, F, G, H, Z>;
export function AsyncContract<A, B, C, D, E, F, G, H, I, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
F: Runtype<F>,
G: Runtype<G>,
H: Runtype<H>,
I: Runtype<I>,
Z: Runtype<Z>,
): AsyncContract9<A, B, C, D, E, F, G, H, I, Z>;
export function AsyncContract<A, B, C, D, E, F, G, H, I, J, Z>(
A: Runtype<A>,
B: Runtype<B>,
C: Runtype<C>,
D: Runtype<D>,
E: Runtype<E>,
F: Runtype<F>,
G: Runtype<G>,
H: Runtype<H>,
I: Runtype<I>,
J: Runtype<J>,
Z: Runtype<Z>,
): AsyncContract10<A, B, C, D, E, F, G, H, I, J, Z>;
export function AsyncContract(...runtypes: Runtype[]) {
const lastIndex = runtypes.length - 1;
const argTypes = runtypes.slice(0, lastIndex);
const returnType = runtypes[lastIndex];
export function AsyncContract<A extends [any, ...any[]] | [], Z>(
argTypes: { [key in keyof A]: key extends 'length' ? A['length'] : RuntypeBase<A[key]> },
returnType: RuntypeBase<Z>,
): AsyncContract<A, Z> {
return {
enforce: (f: (...args: any[]) => any) => (...args: any[]) => {
if (args.length < argTypes.length)
throw new ValidationError(
`Expected ${argTypes.length} arguments but only received ${args.length}`,
if (args.length < argTypes.length) {
return Promise.reject(
new ValidationError(
`Expected ${argTypes.length} arguments but only received ${args.length}`,
),
);
for (let i = 0; i < argTypes.length; i++) argTypes[i].check(args[i]);
}
for (let i = 0; i < argTypes.length; i++) {
const result = argTypes[i].validate(args[i]);
if (result.success) {
argTypes[i] = result.value;
} else {
return Promise.reject(new ValidationError(result.message, result.key));
}
}
const returnedPromise = f(...args);
if (!(returnedPromise instanceof Promise))
throw new ValidationError(
`Expected function to return a promise, but instead got ${returnedPromise}`,
if (!(returnedPromise instanceof Promise)) {
return Promise.reject(
new ValidationError(
`Expected function to return a promise, but instead got ${returnedPromise}`,
),
);
return returnedPromise.then(returnType.check);
}
return returnedPromise.then(value => {
const result = returnType.validate(value);
if (result.success) {
return result.value;
} else {
throw new ValidationError(result.message, result.key);
}
});
},
};
}

0 comments on commit e222f4a

Please sign in to comment.