Skip to content

Instantly share code, notes, and snippets.

@pikax
Created March 11, 2022 21:30
Show Gist options
  • Save pikax/da5832f9a3479c616603c334a261a722 to your computer and use it in GitHub Desktop.
Save pikax/da5832f9a3479c616603c334a261a722 to your computer and use it in GitHub Desktop.
Yup MapSchema
import {
AnyObject,
mixed,
string,
Schema,
AnySchema,
ValidationError,
TestFunction,
} from "yup";
declare type Flags = "s" | "d" | "";
interface SchemaRefDescription {
type: "ref";
key: string;
}
interface SchemaInnerTypeDescription extends SchemaDescription {
innerType?: SchemaFieldDescription;
}
interface SchemaObjectDescription extends SchemaDescription {
fields: Record<string, SchemaFieldDescription>;
}
interface SchemaLazyDescription {
type: string;
label?: string;
meta: object | undefined;
}
declare type SchemaFieldDescription =
| SchemaDescription
| SchemaRefDescription
| SchemaObjectDescription
| SchemaInnerTypeDescription
| SchemaLazyDescription;
interface SchemaDescription {
type: string;
label?: string;
meta: object | undefined;
oneOf: unknown[];
notOneOf: unknown[];
nullable: boolean;
optional: boolean;
tests: Array<{
name?: string;
params: ExtraParams | undefined;
}>;
}
declare type ResolveOptions<TContext = any> = {
value?: any;
parent?: any;
context?: TContext;
};
declare type SchemaSpec<TDefault> = {
coarce: boolean;
nullable: boolean;
optional: boolean;
default?: TDefault | (() => TDefault);
abortEarly?: boolean;
strip?: boolean;
strict?: boolean;
recursive?: boolean;
label?: string | undefined;
meta?: any;
};
interface CastOptions<C = {}> {
parent?: any;
context?: C;
assert?: boolean;
stripUnknown?: boolean;
path?: string;
}
interface Ancester<TContext> {
schema: ISchema<any, TContext>;
value: any;
}
interface NestedTestConfig {
options: InternalOptions<any>;
parent: any;
originalParent: any;
parentPath: string | undefined;
key?: string;
index?: number;
}
interface MessageParams {
path: string;
value: any;
originalValue: any;
label: string;
type: string;
spec: SchemaSpec<any> & Record<string, unknown>;
}
declare type PanicCallback = (err: Error) => void;
declare type NextCallback = (
err: ValidationError[] | ValidationError | null
) => void;
declare type TestOptions<TSchema extends AnySchema = AnySchema> = {
value: any;
path?: string;
label?: string;
options: InternalOptions;
originalValue: any;
schema: TSchema;
sync?: boolean;
spec: MessageParams["spec"];
};
declare type Message<Extra extends Record<string, unknown> = any> =
| string
| ((params: Extra & MessageParams) => unknown)
| Record<PropertyKey, unknown>;
declare type ExtraParams = Record<string, unknown>;
declare type TestConfig<TValue = unknown, TContext = {}> = {
name?: string;
message?: Message<any>;
test: TestFunction<TValue, TContext>;
params?: ExtraParams;
exclusive?: boolean;
skipAbsent?: boolean;
};
declare type Test = ((
opts: TestOptions,
panic: PanicCallback,
next: NextCallback
) => void) & {
OPTIONS?: TestConfig;
};
interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
__flags: F;
__context: C;
__outputType: T;
__default: D;
cast(value: any, options?: CastOptions<C>): T;
validate(value: any, options?: ValidateOptions<C>): Promise<T>;
asNestedTest(config: NestedTestConfig): Test;
describe(options?: ResolveOptions<C>): SchemaFieldDescription;
resolve(options: ResolveOptions<C>): ISchema<T, C, F>;
}
interface ValidateOptions<TContext = {}> {
/**
* Only validate the input, skipping type casting and transformation. Default - false
*/
strict?: boolean;
/**
* Return from validation methods on the first error rather than after all validations run. Default - true
*/
abortEarly?: boolean;
/**
* Remove unspecified keys from objects. Default - false
*/
stripUnknown?: boolean;
/**
* When false validations will not descend into nested schema (relevant for objects or arrays). Default - true
*/
recursive?: boolean;
/**
* Any context needed for validating schema conditions (see: when())
*/
context?: TContext;
}
interface InternalOptions<TContext = {}> extends ValidateOptions<TContext> {
__validating?: boolean;
originalValue?: any;
index?: number;
key?: string;
parent?: any;
path?: string;
sync?: boolean;
from?: Ancester<TContext>[];
}
export class MapSchema<
TKey extends PropertyKey = PropertyKey,
TValue = AnyObject,
TType = Record<TKey, TValue>,
TContext = AnyObject,
TDefault = any,
TFlags extends Flags = ""
> extends Schema<TType, TContext, TDefault, TFlags> {
private _keySchema: Schema;
private _valueSchema: Schema;
// declare __outputType:
public constructor(keySchema: Schema<TKey>, valueSchema: Schema) {
super({
type: "map",
check: (v): v is NonNullable<TType> => {
return v && typeof v === "object";
},
});
this._keySchema = keySchema || string();
this._valueSchema = valueSchema || mixed();
}
protected _typeCheck = (_value: any): _value is NonNullable<TType> => {
return _value && typeof _value === "object";
};
protected _cast(rawValue: any, _options: CastOptions<TContext>): any {
const value = super._cast(rawValue, _options);
const result = {};
Object.entries(value).forEach(([key, value]) => {
result[this._keySchema.cast(key)] = this._valueSchema.cast(value);
});
return result;
}
protected _validate(
_value: any,
options: InternalOptions<TContext> = {},
panic: (err: Error, value: unknown) => void,
next: (err: ValidationError[], value: unknown) => void
): void {
let {
from = [],
originalValue = _value,
recursive = this.spec.recursive,
} = options;
options.from = [
{
schema: this,
value: originalValue,
},
...from,
]; // this flag is needed for handling `strict` correctly in the context of
// validation vs just casting. e.g strict() on a field is only used when validating
options.__validating = true;
options.originalValue = originalValue;
super._validate(_value, options, panic, (objectErrors, value) => {
if (!recursive || !(!value || typeof value !== "object")) {
next(objectErrors, value);
return;
}
originalValue = originalValue || value;
let tests = [];
for (const [key, value] of Object.entries(originalValue)) {
const keyOptions = Object.assign({}, options, {
originalValue: key,
});
const valueOptions = Object.assign({}, options, {
originalValue: value,
});
tests.push(
this._keySchema.asNestedTest({
options: keyOptions,
key,
parent: value,
parentPath: options.path,
originalParent: originalValue,
})
);
tests.push(
this._valueSchema.asNestedTest({
options: valueOptions,
key,
parent: value,
parentPath: options.path,
originalParent: originalValue,
})
);
}
this.runTests(
{
tests,
value,
},
panic,
(fieldErrors) => {
next(fieldErrors.concat(objectErrors), value);
}
);
});
}
}
export function mapSchema<K extends PropertyKey, V>(
keySchema: Schema<K>,
valueSchema: Schema<V>
): MapSchema<K, V> {
return new MapSchema(keySchema, valueSchema);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment