Skip to content

Commit

Permalink
feat: More intuitive Object generics, faster types (#1540)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* WIP

* SAVEPOINT

* WIP

* SAVEPOINT

* SAVEPOINT

* convert tests

* GH actions ci

* test
  • Loading branch information
jquense authored Dec 28, 2021
1 parent eeacc92 commit 912e0be
Show file tree
Hide file tree
Showing 35 changed files with 1,508 additions and 1,088 deletions.
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

0 comments on commit 912e0be

Please sign in to comment.