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

feat: More intuitive Object generics, faster types #1540

Merged
merged 10 commits into from
Dec 28, 2021
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
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"jest/no-focused-tests": "error",
"jest/no-identical-title": "error",
"jest/prefer-to-have-length": "warn",
"jest/valid-expect": "off"
"jest/valid-expect": "off",
"@typescript-eslint/no-empty-function": "off"
}
}
]
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Test
on:
push:
branches: [master, next]
pull_request:
branches: [master, next]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 'lts/*'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn test
2 changes: 1 addition & 1 deletion jest-sync.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"setupFilesAfterEnv": ["./test-setup.js"],
"roots": ["test"],
"testRegex": "\\.(t|j)s$",
"testPathIgnorePatterns": ["helpers\\.js", "\\.eslintrc\\.js", "types\\.ts"]
"testPathIgnorePatterns": ["helpers\\.ts", "\\.eslintrc\\.js", "types\\.ts"]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
],
"testRegex": "\\.(j|t)s$",
"testPathIgnorePatterns": [
"helpers\\.js",
"helpers\\.ts",
"\\.eslintrc\\.js",
"types\\.ts"
]
Expand All @@ -73,6 +73,7 @@
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@types/jest": "^27.0.3",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"babel-eslint": "^10.1.0",
Expand Down
29 changes: 14 additions & 15 deletions src/Condition.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import isSchema from './util/isSchema';
import Reference from './Reference';
import { SchemaLike } from './types';
import type { ISchema } from './util/types';

export interface ConditionBuilder<T extends SchemaLike> {
(this: T, value: any, schema: T): SchemaLike;
(v1: any, v2: any, schema: T): SchemaLike;
(v1: any, v2: any, v3: any, schema: T): SchemaLike;
(v1: any, v2: any, v3: any, v4: any, schema: T): SchemaLike;
export interface ConditionBuilder<T extends ISchema<any, any>> {
(this: T, value: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, v3: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, v3: any, v4: any, schema: T): ISchema<any, any> | void;
}

export type ConditionConfig<T extends SchemaLike> = {
export type ConditionConfig<T extends ISchema<any>> = {
is: any | ((...values: any[]) => boolean);
then?: SchemaLike | ((schema: T) => SchemaLike);
otherwise?: SchemaLike | ((schema: T) => SchemaLike);
then?: (schema: T) => ISchema<any>;
otherwise?: (schema: T) => ISchema<any>;
};

export type ConditionOptions<T extends SchemaLike> =
export type ConditionOptions<T extends ISchema<any, any>> =
| ConditionBuilder<T>
| ConditionConfig<T>;

Expand All @@ -25,7 +25,7 @@ export type ResolveOptions<TContext = any> = {
context?: TContext;
};

class Condition<T extends SchemaLike = SchemaLike> {
class Condition<T extends ISchema<any, any> = ISchema<any, any>> {
fn: ConditionBuilder<T>;

constructor(public refs: Reference[], options: ConditionOptions<T>) {
Expand All @@ -52,18 +52,17 @@ class Condition<T extends SchemaLike = SchemaLike> {
: (...values: any[]) => values.every((value) => value === is);

this.fn = function (...args: any[]) {
let options = args.pop();
let _opts = args.pop();
let schema = args.pop();
let branch = check(...args) ? then : otherwise;

if (!branch) return undefined;
if (typeof branch === 'function') return branch(schema);
return schema.concat(branch.resolve(options));
return branch?.(schema) ?? schema;
};
}

resolve(base: T, options: ResolveOptions) {
let values = this.refs.map((ref) =>
// TODO: ? operator here?
ref.getValue(options?.value, options?.parent, options?.context),
);

Expand Down
92 changes: 47 additions & 45 deletions src/Lazy.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,130 @@
import isSchema from './util/isSchema';
import type { Callback, ValidateOptions } from './types';
import type { AnyObject, Callback, ValidateOptions } from './types';
import type { ResolveOptions } from './Condition';

import type {
AnySchema,
CastOptions,
ConfigOf,
SchemaFieldDescription,
SchemaLazyDescription,
} from './schema';
import { Config, TypedSchema, TypeOf } from './util/types';

export type LazyBuilder<T extends AnySchema = any> = (
import { Flags, ISchema } from './util/types';
import { BaseSchema } from '.';

export type LazyBuilder<
T,
TContext = AnyObject,
TDefault = any,
TFlags extends Flags = any,
> = (
value: any,
options: ResolveOptions,
) => T;

export function create<T extends AnySchema>(builder: LazyBuilder<T>) {
return new Lazy(builder);
) => ISchema<T, TContext, TFlags, TDefault>;

export function create<
T,
TContext = AnyObject,
TFlags extends Flags = any,
TDefault = any,
>(builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
return new Lazy<T, TContext, TDefault, TFlags>(builder);
}

export type LazyReturnValue<T> = T extends Lazy<infer TSchema>
? TSchema
: never;

export type LazyType<T> = LazyReturnValue<T> extends TypedSchema
? TypeOf<LazyReturnValue<T>>
: never;

export interface LazySpec {
meta: Record<string, unknown> | undefined;
}

class Lazy<T extends AnySchema, TConfig extends Config = ConfigOf<T>>
implements TypedSchema {
class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
implements ISchema<T, TContext, TFlags, TDefault>
{
type = 'lazy' as const;

__isYupSchema__ = true;

readonly __type!: T['__type'];
readonly __outputType!: T['__outputType'];
declare readonly __outputType: T;
declare readonly __context: TContext;
declare readonly __flags: TFlags;
declare readonly __default: TDefault;

spec: LazySpec;

constructor(private builder: LazyBuilder<T>) {
constructor(private builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
this.spec = { meta: undefined };
}

clone(): Lazy<T, TConfig> {
const next = new Lazy(this.builder);
clone(): Lazy<T, TContext, TDefault, TFlags> {
const next = create(this.builder);
next.spec = { ...this.spec };
return next;
}

private _resolve = (
value: any,
options: ResolveOptions<TConfig['context']> = {},
): T => {
let schema = this.builder(value, options);
options: ResolveOptions<TContext> = {},
): BaseSchema<T, TContext, TDefault, TFlags> => {
let schema = this.builder(value, options) as BaseSchema<
T,
TContext,
TDefault,
TFlags
>;

if (!isSchema(schema))
throw new TypeError('lazy() functions must return a valid schema');

return schema.resolve(options);
};

resolve(options: ResolveOptions<TConfig['context']>) {
resolve(options: ResolveOptions<TContext>) {
return this._resolve(options.value, options);
}

cast(value: any, options?: CastOptions<TConfig['context']>): T['__type'] {
cast(value: any, options?: CastOptions<TContext>): T {
return this._resolve(value, options).cast(value, options);
}

validate(
value: any,
options?: ValidateOptions,
maybeCb?: Callback,
): T['__outputType'] {
): Promise<T> {
// @ts-expect-error missing public callback on type
return this._resolve(value, options).validate(value, options, maybeCb);
}

validateSync(
value: any,
options?: ValidateOptions<TConfig['context']>,
): T['__outputType'] {
validateSync(value: any, options?: ValidateOptions<TContext>): T {
return this._resolve(value, options).validateSync(value, options);
}

validateAt(
path: string,
value: any,
options?: ValidateOptions<TConfig['context']>,
) {
validateAt(path: string, value: any, options?: ValidateOptions<TContext>) {
return this._resolve(value, options).validateAt(path, value, options);
}

validateSyncAt(
path: string,
value: any,
options?: ValidateOptions<TConfig['context']>,
options?: ValidateOptions<TContext>,
) {
return this._resolve(value, options).validateSyncAt(path, value, options);
}

isValid(value: any, options?: ValidateOptions<TConfig['context']>) {
isValid(value: any, options?: ValidateOptions<TContext>) {
return this._resolve(value, options).isValid(value, options);
}

isValidSync(value: any, options?: ValidateOptions<TConfig['context']>) {
isValidSync(value: any, options?: ValidateOptions<TContext>) {
return this._resolve(value, options).isValidSync(value, options);
}

describe(
options?: ResolveOptions<TConfig['context']>,
options?: ResolveOptions<TContext>,
): SchemaLazyDescription | SchemaFieldDescription {
return options
? this.resolve(options).describe(options)
: { type: 'lazy', meta: this.spec.meta, label: undefined };
}

meta(): Record<string, unknown> | undefined;
meta(obj: Record<string, unknown>): Lazy<T, TConfig>;
meta(obj: Record<string, unknown>): Lazy<T, TContext, TDefault, TFlags>;
meta(...args: [Record<string, unknown>?]) {
if (args.length === 0) return this.spec.meta;

Expand Down
Loading