Skip to content

Commit

Permalink
Merge 9daf95e into 64a93ec
Browse files Browse the repository at this point in the history
  • Loading branch information
ForbesLindesay committed Sep 11, 2020
2 parents 64a93ec + 9daf95e commit 4b06f80
Show file tree
Hide file tree
Showing 34 changed files with 1,512 additions and 292 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ In addition to providing a `check` method, funtypes can be used as [type guards]

```ts
function disembark(obj: {}) {
if (SpaceObject.guard(obj)) {
if (SpaceObject.test(obj)) {
// obj: SpaceObject
if (obj.type === 'ship') {
// obj: Ship
Expand Down Expand Up @@ -265,7 +265,7 @@ One important choice when changing `Constraint` static types is choosing the
correct underlying type. The implementation of `Constraint` will validate the
underlying type *before* running your constraint function. So it's important to
use a lowest-common-denominator type that will pass validation for all expected
inputs of your constraint function or type guard. If there's no obvious
inputs of your constraint function or type test. If there's no obvious
lowest-common-denominator type, you can always use `Unknown` as the underlying
type, as shown in the `Buffer` examples above.

Expand Down
21 changes: 14 additions & 7 deletions src/asynccontract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { ValidationError } from './errors';
import { RuntypeBase } from './runtype';
import {
createGuardVisitedState,
createVisitedState,
innerGuard,
innerValidate,
OpaqueVisitedState,
RuntypeBase,
} from './runtype';

export interface AsyncContract<A extends any[], Z> {
enforce(f: (...a: A) => Promise<Z>): (...a: A) => Promise<Z>;
Expand All @@ -21,10 +28,11 @@ export function AsyncContract<A extends [any, ...any[]] | [], Z>(
),
);
}
const visited: OpaqueVisitedState = createVisitedState();
for (let i = 0; i < argTypes.length; i++) {
const result = argTypes[i].validate(args[i]);
const result = innerValidate(argTypes[i], args[i], visited);
if (result.success) {
argTypes[i] = result.value;
args[i] = result.value;
} else {
return Promise.reject(new ValidationError(result.message, result.key));
}
Expand All @@ -38,12 +46,11 @@ export function AsyncContract<A extends [any, ...any[]] | [], Z>(
);
}
return returnedPromise.then(value => {
const result = returnType.validate(value);
if (result.success) {
return result.value;
} else {
const result = innerGuard(returnType, value, createGuardVisitedState());
if (result) {
throw new ValidationError(result.message, result.key);
}
return value;
});
},
};
Expand Down
22 changes: 15 additions & 7 deletions src/contract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { RuntypeBase } from './runtype';
import {
createGuardVisitedState,
createVisitedState,
innerGuard,
innerValidate,
OpaqueVisitedState,
RuntypeBase,
} from './runtype';
import { ValidationError } from './errors';

export interface Contract<A extends any[], Z> {
Expand All @@ -18,20 +25,21 @@ export function Contract<A extends [any, ...any[]] | [], Z>(
throw new ValidationError(
`Expected ${argTypes.length} arguments but only received ${args.length}`,
);
const visited: OpaqueVisitedState = createVisitedState();
for (let i = 0; i < argTypes.length; i++) {
const result = argTypes[i].validate(args[i]);
const result = innerValidate(argTypes[i], args[i], visited);
if (result.success) {
argTypes[i] = result.value;
args[i] = result.value;
} else {
throw new ValidationError(result.message, result.key);
}
}
const result = returnType.validate(f(...args));
if (result.success) {
return result.value;
} else {
const rawResult = f(...args);
const result = innerGuard(returnType, rawResult, createGuardVisitedState());
if (result) {
throw new ValidationError(result.message, result.key);
}
return rawResult;
},
};
}
6 changes: 4 additions & 2 deletions src/decorator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ValidationError } from './errors';
import { RuntypeBase } from './runtype';
import { createVisitedState, innerValidate, OpaqueVisitedState, RuntypeBase } from './runtype';

type PropKey = string | symbol;
const prototypes = new WeakMap<any, Map<PropKey, number[]>>();
Expand Down Expand Up @@ -75,16 +75,18 @@ export function checked(...runtypes: RuntypeBase<unknown>[]) {
throw new Error('Number of `@checked` runtypes exceeds actual parameter length.');
}

const visited: OpaqueVisitedState = createVisitedState();
descriptor.value = function(...args: any[]) {
runtypes.forEach((type, typeIndex) => {
const parameterIndex = validParameterIndices[typeIndex];
const validated = type.validate(args[parameterIndex]);
const validated = innerValidate(type, args[parameterIndex], visited);
if (!validated.success) {
throw new ValidationError(
`${methodId}, argument #${parameterIndex}: ${validated.message}`,
validated.key,
);
}
args[parameterIndex] = validated.value;
});
return method.apply(this, args);
};
Expand Down
10 changes: 5 additions & 5 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ describe('change static type with Constraint', () => {
name: 'SomeClass',
});

if (C.guard(value)) {
if (C.test(value)) {
return value;
} else {
return new SomeClassV2(3);
Expand Down Expand Up @@ -765,7 +765,7 @@ describe('change static type with Constraint', () => {
| InstanceOf<Constructor<never>>
| Brand<string, String | Number>,
) => {
const check = <A>(X: Runtype<A>): A => X.check({});
const check = <A>(X: Runtype<A>): A => X.parse({});

switch (X.tag) {
case 'unknown':
Expand Down Expand Up @@ -828,18 +828,18 @@ function expectLiteralField<O, K extends keyof O, V extends O[K]>(o: O, k: K, v:
}

function assertAccepts<A>(value: unknown, runtype: Runtype<A>) {
const result = runtype.validate(value);
const result = runtype.safeParse(value);
if (result.success === false) fail(result.message);
}

function assertRejects<A>(value: unknown, runtype: Runtype<A>) {
const result = runtype.validate(value);
const result = runtype.safeParse(value);
if (result.success === true) fail('value passed validation even though it was not expected to');
}

function assertThrows<A>(value: unknown, runtype: Runtype<A>, error: string, key?: string) {
try {
runtype.check(value);
runtype.parse(value);
fail('value passed validation even though it was not expected to');
} catch (exception) {
const { message: errorMessage, key: errorKey } = exception;
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { Runtype, Static } from './runtype';
export { Runtype, Codec, Static } from './runtype';
export * from './result';
export * from './contract';
export * from './asynccontract';
Expand All @@ -22,4 +22,5 @@ export { InstanceOf } from './types/instanceof';
export * from './types/lazy';
export * from './types/constraint';
export { Brand } from './types/brand';
export { ParsedValue } from './types/ParsedValue';
export * from './decorator';
29 changes: 16 additions & 13 deletions src/runtype.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { String, Number, Record } from './';

test('Runtype.validate', () => {
expect(String.validate('hello')).toMatchInlineSnapshot(`
expect(String.safeParse('hello')).toMatchInlineSnapshot(`
Object {
"success": true,
"value": "hello",
}
`);
expect(String.validate(42)).toMatchInlineSnapshot(`
expect(String.safeParse(42)).toMatchInlineSnapshot(`
Object {
"message": "Expected string, but was number",
"success": false,
Expand All @@ -20,6 +20,9 @@ test('Runtype.assert', () => {
expect(() => String.assert(42)).toThrowErrorMatchingInlineSnapshot(
`"Expected string, but was number"`,
);
expect(() => Record({ value: String }).assert({ value: 42 })).toThrowErrorMatchingInlineSnapshot(
`"Expected string, but was number in value"`,
);
});

test('Runtype.assert', () => {
Expand All @@ -30,37 +33,37 @@ test('Runtype.assert', () => {
});

test('Runtype.check', () => {
expect(String.check('hello')).toBe('hello');
expect(() => String.check(42)).toThrowErrorMatchingInlineSnapshot(
expect(String.parse('hello')).toBe('hello');
expect(() => String.parse(42)).toThrowErrorMatchingInlineSnapshot(
`"Expected string, but was number"`,
);
});

test('Runtype.guard', () => {
expect(String.guard('hello')).toBe(true);
expect(String.guard(42)).toBe(false);
test('Runtype.test', () => {
expect(String.test('hello')).toBe(true);
expect(String.test(42)).toBe(false);
});

test('Runtype.Or', () => {
expect(String.Or(Number).guard('hello')).toBe(true);
expect(String.Or(Number).guard(42)).toBe(true);
expect(String.Or(Number).guard(true)).toBe(false);
expect(String.Or(Number).test('hello')).toBe(true);
expect(String.Or(Number).test(42)).toBe(true);
expect(String.Or(Number).test(true)).toBe(false);
});

test('Runtype.And', () => {
expect(
Record({ a: String })
.And(Record({ b: Number }))
.guard({ a: 'hello', b: 42 }),
.test({ a: 'hello', b: 42 }),
).toBe(true);
expect(
Record({ a: String })
.And(Record({ b: Number }))
.guard({ a: 42, b: 42 }),
.test({ a: 42, b: 42 }),
).toBe(false);
expect(
Record({ a: String })
.And(Record({ b: Number }))
.guard({ a: 'hello', b: 'hello' }),
.test({ a: 'hello', b: 'hello' }),
).toBe(false);
});

0 comments on commit 4b06f80

Please sign in to comment.