Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refinements and integer Cake #65

Merged
merged 3 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ seven.is(8); // false
- [`array`](#array)
- [`boolean`](#boolean)
- [`bigint`](#bigint)
- [`integer`](#integer)
- [`never`](#never)
- [`number`](#number)
- [`string`](#string)
Expand Down Expand Up @@ -809,6 +810,17 @@ bigint.is(5); // false

---

#### `integer`

Like [number](#number), but only allow numbers with integer values.

```ts
integer.is(5); // true
integer.is(5.5); // false
```

---

#### `never`

A [Cake](#cake) representing the `never` type. No value satisfies this type.
Expand Down Expand Up @@ -2020,6 +2032,10 @@ const c: Class<Date, [number]> = Date;

### Unreleased

#### Added

- [integer](#integer) Cake

#### Changed

- [number](#number) no longer accepts `NaN` ([#64](https://github.com/justinyaodu/caketype/pull/64))
Expand Down
46 changes: 46 additions & 0 deletions etc/caketype.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export abstract class Cake<in out T = any> extends Untagged implements CakeArgs
//
// (undocumented)
readonly options: Partial<CheckOptions>;
// (undocumented)
refined<O extends T, R extends Refinement<T, O>>(refinement: R): RefinementCake<T, O, this, R>;
toString(): string;
// (undocumented)
abstract withName(name: string | null): Cake<T>;
Expand Down Expand Up @@ -244,6 +246,17 @@ export type If<T extends boolean, U, V> = T extends true ? U : V;
// @public
export type Infer<C extends Cake> = C extends Cake<infer T> ? T : never;

// @public
export const integer: RefinementCake<number, number, NumberCake, IntegerRefinement>;

// @public (undocumented)
export class IntegerRefinement extends Refinement<number> {
// (undocumented)
dispatchCheck(value: number): CakeError | null;
// (undocumented)
toString(): string;
}

// @public
export function isPrimitive(value: unknown): value is Primitive;

Expand Down Expand Up @@ -518,6 +531,39 @@ export interface ReferenceCakeArgs<C extends Cake> extends CakeArgs {
readonly get: () => C;
}

// @public (undocumented)
export abstract class Refinement<in I = any, out O extends I = I> {
// (undocumented)
check(value: I): Result<O, CakeError>;
// (undocumented)
abstract dispatchCheck(value: I): CakeError | null;
// (undocumented)
abstract toString(): string;
}

// @public (undocumented)
export class RefinementCake<I, O extends I, B extends Cake<I>, R extends Refinement<I, O>> extends Cake<O> implements RefinementCakeArgs<I, O, B, R> {
constructor(args: RefinementCakeArgs<I, O, B, R>);
// (undocumented)
readonly base: B;
// (undocumented)
dispatchCheck(value: unknown, context: CakeDispatchCheckContext): CakeError | null;
// (undocumented)
dispatchStringify(context: CakeDispatchStringifyContext): string;
// (undocumented)
readonly refinement: R;
// (undocumented)
withName(name: string | null): RefinementCake<I, O, B, R>;
}

// @public (undocumented)
export interface RefinementCakeArgs<I, O extends I, B extends Cake<I>, R extends Refinement<I, O>> extends CakeArgs {
// (undocumented)
readonly base: B;
// (undocumented)
readonly refinement: R;
}

// @public (undocumented)
export class RequiredPropertyMissingCakeError extends CakeError {
constructor(cake: Cake);
Expand Down
8 changes: 8 additions & 0 deletions src/cake/Cake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
CakeStringifier,
Checker,
CheckOptions,
Refinement,
RefinementCake,
Untagged,
} from "./index-internal";
import type {
Expand Down Expand Up @@ -234,6 +236,12 @@ abstract class Cake<in out T = any> extends Untagged implements CakeArgs {
return this.checkShape(value).ok;
}

refined<O extends T, R extends Refinement<T, O>>(
refinement: R
): RefinementCake<T, O, this, R> {
return new RefinementCake({ base: this, refinement });
}

/**
* Return a human-readable string representation of the type represented by
* this Cake.
Expand Down
51 changes: 51 additions & 0 deletions src/cake/IntegerRefinement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
CakeError,
CakeErrorDispatchFormatContext,
number,
Refinement,
StringTree,
} from "./index-internal";

/**
* @see {@link integer}.
*
* @public
*/
class IntegerRefinement extends Refinement<number> {
dispatchCheck(value: number): CakeError | null {
if (!Number.isInteger(value)) {
return new NotAnIntegerCakeError(value);
}
return null;
}

toString(): string {
return "is an integer";
}
}

class NotAnIntegerCakeError extends CakeError {
constructor(readonly value: number) {
super();
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
dispatchFormat(context: CakeErrorDispatchFormatContext): StringTree {
return "Number is not an integer.";
}
}

/**
* Like {@link number}, but only allow numbers with integer values.
*
* @example
* ```ts
* integer.is(5); // true
* integer.is(5.5); // false
* ```
*
* @public
*/
const integer = number.refined(new IntegerRefinement()).withName("integer");

export { IntegerRefinement, integer };
23 changes: 23 additions & 0 deletions src/cake/Refinement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Result } from "../index-internal";

import type { CakeError } from "./index-internal";

/**
* @public
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abstract class Refinement<in I = any, out O extends I = I> {
check(value: I): Result<O, CakeError> {
const error = this.dispatchCheck(value);
if (error === null) {
return Result.ok(value as O);
}
return Result.err(error);
}

abstract dispatchCheck(value: I): CakeError | null;

abstract toString(): string;
}

export { Refinement };
66 changes: 66 additions & 0 deletions src/cake/RefinementCake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
Cake,
CakeArgs,
CakeError,
CakeDispatchCheckContext,
CakeDispatchStringifyContext,
Refinement,
} from "./index-internal";

/**
* @public
*/
interface RefinementCakeArgs<
I,
O extends I,
B extends Cake<I>,
R extends Refinement<I, O>
> extends CakeArgs {
readonly base: B;
readonly refinement: R;
}

/**
* @public
*/
class RefinementCake<
I,
O extends I,
B extends Cake<I>,
R extends Refinement<I, O>
>
extends Cake<O>
implements RefinementCakeArgs<I, O, B, R>
{
readonly base: B;
readonly refinement: R;

constructor(args: RefinementCakeArgs<I, O, B, R>) {
super(args);
this.base = args.base;
this.refinement = args.refinement;
}

dispatchCheck(
value: unknown,
context: CakeDispatchCheckContext
): CakeError | null {
const { recurse } = context;
const baseError = recurse(this.base, value);
if (baseError !== null) {
return baseError;
}
return this.refinement.check(value as I).errorOr(null);
}

dispatchStringify(context: CakeDispatchStringifyContext): string {
const { recurse } = context;
return `(${recurse(this.base)}).refined(${this.refinement.toString()})`;
}

withName(name: string | null): RefinementCake<I, O, B, R> {
return new RefinementCake({ ...this, name });
}
}

export { RefinementCake, RefinementCakeArgs };
3 changes: 3 additions & 0 deletions src/cake/index-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ export * from "./LiteralCake";
export * from "./NumberCake";
export * from "./ObjectCake";
export * from "./ReferenceCake";
export * from "./Refinement";
export * from "./RefinementCake";
export * from "./StringTree";
export * from "./TupleCake";
export * from "./TypeGuardCake";
export * from "./UnionCake";

export * from "./ArrayCake";
export * from "./IntegerRefinement";

export * from "./helper-types";
8 changes: 8 additions & 0 deletions src/cake/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export {
// Checker.ts
Checker,
CircularReferenceCakeError,
// IntegerRefinement.ts
IntegerRefinement,
integer,
// LiteralCake.ts
LiteralCake,
LiteralCakeArgs,
Expand All @@ -38,6 +41,11 @@ export {
ReferenceCake,
ReferenceCakeArgs,
reference,
// Refinement.ts
Refinement,
// RefinementCake.ts
RefinementCake,
RefinementCakeArgs,
// StringTree.ts
StringTree,
// tags.ts
Expand Down
23 changes: 23 additions & 0 deletions tests/cake/IntegerRefinement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { integer } from "../../src";
import { expectTypeError } from "../test-helpers";

describe("documentation examples", () => {
test("integer", () => {
expect(integer.is(5)).toStrictEqual(true);
expect(integer.is(5.5)).toStrictEqual(false);
});
});

test("not integer error message", () => {
expectTypeError(() => integer.as(5.5), "Number is not an integer.");
});

test("wrong type", () => {
expect(integer.is("oops")).toStrictEqual(false);
});

test("stringify when name removed", () => {
expect(integer.withName(null).toString()).toStrictEqual(
"(number).refined(is an integer)"
);
});