Created
March 11, 2022 21:30
-
-
Save pikax/da5832f9a3479c616603c334a261a722 to your computer and use it in GitHub Desktop.
Yup MapSchema
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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