Skip to content

Commit

Permalink
feat: greatly rework how assertions and brands work
Browse files Browse the repository at this point in the history
  • Loading branch information
yamiteru committed Apr 28, 2024
1 parent f1fa54c commit 4508e58
Show file tree
Hide file tree
Showing 76 changed files with 604 additions and 302 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"build:stats": "bun run scripts/stats.ts",
"build:imports": "bun run scripts/imports.ts",
"build:website": "typedoc --basePath ./ --entryPoints src/index.ts --out website",
"test": "vitest run --typecheck",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"check": "bunx @biomejs/biome check --apply ./"
},
Expand All @@ -70,6 +70,6 @@
"access": "public"
},
"dependencies": {
"@the-minimal/types": "0.2.2"
"@the-minimal/types": "0.3.6"
}
}
16 changes: 4 additions & 12 deletions src/assertions/and/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import type {
AndSchema,
InferAndOutput,
InferAndSchema,
} from "@assertions/and/types";
import type { Assertion } from "@the-minimal/types";
import type { AndSchema, Validate } from "@assertions/and/types";

/**
* Checks if all the assertions pass.
Expand All @@ -23,12 +18,9 @@ import type { Assertion } from "@the-minimal/types";
* userEmail("[email protected]"); // passes
* ```
*/
export const and =
<const $Schema extends AndSchema>(
assertions: InferAndSchema<$Schema>,
): Assertion<InferAndOutput<$Schema>> =>
(v) => {
export const and = <const $Schema extends AndSchema>(assertions: $Schema) =>
((v: unknown) => {
for (let i = 0; i < assertions.length; ++i) {
((assertions as any)[i] as any)(v);
}
};
}) as unknown as Validate.And<$Schema>;
47 changes: 39 additions & 8 deletions src/assertions/and/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import type { Assertion } from "@the-minimal/types";
import { and } from "@assertions/and/index";
import type { AnyBrand, Brand, None } from "@the-minimal/types";

export type AndSchema = Array<unknown>;
export namespace Validate {
export type And<$Types extends AndSchema> = Brand<
"and",
$Schema,
{
input: InferAndInputs<FilterAndInputs<$Types>>;
output: InferAndOutputs<$Types>;
}
>;
}

export type InferAndSchema<$Schema extends AndSchema> = {
[$Key in keyof $Schema]: Assertion<$Schema[$Key]>;
};
export type AndSchema = AnyBrand[];

export type InferAndOutput<$Schema extends AndSchema> = $Schema extends [
export type InferAndInputs<$Types extends unknown[]> = $Types extends [
infer $Head,
...infer $Tail,
]
? $Tail extends [infer $1, ...infer $2]
? $Head & InferAndOutput<$Tail>
? $Tail extends [infer _1, ...infer _2]
? $Head & InferAndOutputs<$Tail>
: $Head
: never;

type FilterAndInputs<$Schema extends AndSchema> = $Schema extends readonly [
infer $Head,
...infer $Tail,
]
? $Tail extends readonly [infer _1, ...infer _2]
? $Head extends Brand<any, any, { input: infer $Input; output: any }>
? [$Input, ...FilterAndInputs<$Tail>]
: []
: $Head extends Brand<any, any, { input: infer $Input; output: any }>
? [$Input]
: []
: [];

export type InferAndOutputs<$Schema extends AndSchema> =
$Schema extends readonly [infer $Head, ...infer $Tail]
? $Tail extends readonly [infer _1, ...infer _2]
? ($Head extends Brand<any, any, { input: infer $Input; output: any }>
? $Input
: $Head) &
InferAndOutputs<$Tail>
: $Head
: never;
20 changes: 10 additions & 10 deletions src/assertions/and2/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Assertion } from "@the-minimal/types";
import type { Validate } from "@assertions/and/types";
import type { AnyBrand } from "@the-minimal/types";

/**
* Checks that both assertions pass.
Expand All @@ -15,12 +16,11 @@ import type { Assertion } from "@the-minimal/types";
* userEmail("[email protected]"); // passes
* ```
*/
export const and2 =
<$Input1, $Input2>(
assertion1: Assertion<$Input1>,
assertion2: Assertion<$Input2>,
): Assertion<$Input1 & $Input2> =>
(v) => {
assertion1(v);
assertion2(v);
};
export const and2 = <$Brand1 extends AnyBrand, $Brand2 extends AnyBrand>(
brand1: $Brand1,
brand2: $Brand2,
) =>
((v: unknown) => {
(brand1 as any)(v);
(brand2 as any)(v);
}) as unknown as Validate.And<[$Brand1, $Brand2]>;
28 changes: 16 additions & 12 deletions src/assertions/and3/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Assertion } from "@the-minimal/types";
import type { Validate } from "@assertions/and/types";
import type { AnyBrand } from "@the-minimal/types";

/**
* Checks that all three assertions pass.
Expand All @@ -17,14 +18,17 @@ import type { Assertion } from "@the-minimal/types";
* userEmail("[email protected]"); // passes
* ```
*/
export const and3 =
<$Input1, $Input2, $Input3>(
assertion1: Assertion<$Input1>,
assertion2: Assertion<$Input2>,
assertion3: Assertion<$Input3>,
): Assertion<$Input1 & $Input2 & $Input3> =>
(v) => {
assertion1(v);
assertion2(v);
assertion3(v);
};
export const and3 = <
$Brand1 extends AnyBrand,
$Brand2 extends AnyBrand,
$Brand3 extends AnyBrand,
>(
brand1: $Brand1,
brand2: $Brand2,
brand3: $Brand3,
) =>
((v: unknown) => {
(brand1 as any)(v);
(brand2 as any)(v);
(brand3 as any)(v);
}) as unknown as Validate.And<[$Brand1, $Brand2, $Brand3]>;
14 changes: 7 additions & 7 deletions src/assertions/array/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isArray } from "@assertions/isArray";
import type { Assertion } from "@the-minimal/types";
import type { AnyBrand } from "@the-minimal/types";
import type { Validate } from "./types";

/**
* Checks that assertion passes for each element of the array.
*
* @param assertion - Assertion to be applied to each element of the array.
* @param brand - Assertion to be applied to each element of the array.
*
* @example
* ```ts
Expand All @@ -14,12 +15,11 @@ import type { Assertion } from "@the-minimal/types";
* numbers([1, 2, 3]); // passes
* ```
*/
export const array =
<$Input>(assertion: Assertion<$Input>): Assertion<Array<$Input>> =>
(v) => {
export const array = <$Brand extends AnyBrand>(brand: $Brand) =>
((v: unknown) => {
isArray(v);

for (let i = 0; i < v.length; ++i) {
assertion((v as any)[i]);
(brand as any)((v as any)[i]);
}
};
}) as unknown as Validate.Array<$Brand>;
13 changes: 13 additions & 0 deletions src/assertions/array/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { AnyBrand, Brand, None } from "@the-minimal/types";
import type { InferInput } from "@types";

export namespace Validate {
export type Array<$Brand extends AnyBrand> = Brand<
"array",
$Brand,
{
input: InferInput<$Brand>[];
output: None;
}
>;
}
5 changes: 3 additions & 2 deletions src/assertions/boolean/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { boolean } from "@assertions/boolean";
import { fc, test } from "@fast-check/vitest";
import { assert } from "@utils";
import { expect } from "vitest";

test.prop([fc.boolean()])(
"should not throw if value is of type boolean",
(value) => {
expect(() => boolean(value)).not.toThrow();
expect(() => assert(boolean, value)).not.toThrow();
},
);

test.prop([fc.string()])(
"should throw if value is not of type boolean",
(value) => {
expect(() => boolean(value)).toThrow();
expect(() => assert(boolean, value)).toThrow();
},
);
3 changes: 2 additions & 1 deletion src/assertions/boolean/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type } from "@assertions/type";
import type { Validate } from "./types";

/**
* Checks that the value is a boolean.
Expand All @@ -9,4 +10,4 @@ import { type } from "@assertions/type";
* boolean(true); // passes
* ```
*/
export const boolean = type<boolean>("boolean");
export const boolean: Validate.Type.Boolean = type("boolean");
14 changes: 14 additions & 0 deletions src/assertions/boolean/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Brand } from "@the-minimal/types";

export namespace Validate {
export namespace Type {
export type Boolean = Brand<
"type",
"boolean",
{
input: boolean;
output: boolean;
}
>;
}
}
5 changes: 2 additions & 3 deletions src/assertions/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Email } from "@assertions/email/types";
import { regex } from "@assertions/regex";
import type { Assertion } from "@the-minimal/types";
import type { Validate } from "./types";

/**
* Checks if value matches email RegExp.
Expand All @@ -11,4 +10,4 @@ import type { Assertion } from "@the-minimal/types";
* email("[email protected]"); // passes
* ```
*/
export const email: Assertion<Email> = regex(/^\w+@.+\..+$/);
export const email: Validate.Regex.Email = regex(/^\w+@.+\..+$/);
8 changes: 6 additions & 2 deletions src/assertions/email/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { Regex } from "@assertions/regex/types";
import type { Brand } from "@the-minimal/types";

export type Email = Regex<"email">;
export namespace Validate {
export namespace Regex {
export type Email = Brand<"regex", "email">;
}
}
15 changes: 7 additions & 8 deletions src/assertions/endsWith/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { EndsWith } from "@assertions/endsWith/types";
import { error } from "@error";
import type { Assertion } from "@the-minimal/types";
import type { Validate } from "./types";

/**
* Checks if value ends with `searchString`.
Expand All @@ -17,9 +16,9 @@ import type { Assertion } from "@the-minimal/types";
* question("really?"); // passes
* ```
*/
export const endsWith =
<$SearchString extends string>(
searchString: $SearchString,
): Assertion<EndsWith<$SearchString>> =>
(v: any) =>
v.endsWith(searchString) || error(endsWith);
export const endsWith = <$SearchString extends string>(
searchString: $SearchString,
) =>
((v: any) =>
v.endsWith(searchString) ||
error(endsWith)) as unknown as Validate.String.EndsWith<$SearchString>;
9 changes: 8 additions & 1 deletion src/assertions/endsWith/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import type { Brand } from "@the-minimal/types";

export type EndsWith<$Input> = Brand<"EndsWith", $Input>;
export namespace Validate {
export namespace String {
export type EndsWith<$Type extends string> = Brand<
"string-endswith",
$Type
>;
}
}
11 changes: 5 additions & 6 deletions src/assertions/includes/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Includes } from "@assertions/includes/types";
import type { Validate } from "@assertions/includes/types";
import { error } from "@error";
import type { Assertion } from "@the-minimal/types";

/**
* Checks if value includes with another value.
Expand All @@ -15,7 +14,7 @@ import type { Assertion } from "@the-minimal/types";
* hello("--hello--"); // passes
* ```
*/
export const includes =
<$Input>(input: $Input): Assertion<Includes<$Input>> =>
(v: any) =>
v.includes(input) || error(includes);
export const includes = <const $Input>(input: $Input) =>
((v: any) =>
v.includes(input) ||
error(includes)) as unknown as Validate.List.Includes<$Input>;
6 changes: 5 additions & 1 deletion src/assertions/includes/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { Brand } from "@the-minimal/types";

export type Includes<$Input> = Brand<"Includes", $Input>;
export namespace Validate {
export namespace List {
export type Includes<$Input> = Brand<"list-includes", $Input>;
}
}
7 changes: 3 additions & 4 deletions src/assertions/integer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Integer } from "@assertions/integer/types";
import type { Validate } from "@assertions/integer/types";
import { error } from "@error";
import type { Assertion } from "@the-minimal/types";

/**
* Checks if value is integer.
Expand All @@ -11,5 +10,5 @@ import type { Assertion } from "@the-minimal/types";
* integer(1) // passes
* ```
*/
export const integer: Assertion<Integer> = (v) =>
Number.isInteger(v) || error(integer);
export const integer = ((v: unknown) =>
Number.isInteger(v) || error(integer)) as unknown as Validate.Number.Integer;
6 changes: 5 additions & 1 deletion src/assertions/integer/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { Brand } from "@the-minimal/types";

export type Integer = Brand<"Integer">;
export namespace Validate {
export namespace Number {
export type Integer = Brand<"number-integer">;
}
}
6 changes: 3 additions & 3 deletions src/assertions/lazy/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UnknownAssertion } from "@the-minimal/types";
import type { AnyBrand } from "@the-minimal/types";

/**
* Wraps assertion in a function that will be evaluated only when the assertion is called.
Expand All @@ -20,9 +20,9 @@ import type { UnknownAssertion } from "@the-minimal/types";
* }); // passes
* ```
*/
export const lazy = <$Validation extends UnknownAssertion>(
export const lazy = <$Validation extends AnyBrand>(
assertion: (input: unknown) => $Validation,
) =>
((input: unknown) => {
(assertion(input) as any)(input);
}) as $Validation;
}) as unknown as $Validation;
Loading

0 comments on commit 4508e58

Please sign in to comment.