Commit f1b9f7f2 by Vladislav Lagunov

Печать названия проблемных полей в printProblem

parent 4c6c4a99
...@@ -84,15 +84,6 @@ export function fromString(d: Decoder<any>, value: string): unknown { ...@@ -84,15 +84,6 @@ export function fromString(d: Decoder<any>, value: string): unknown {
return value.split(', ').map(x => fromString(d._decoder, x)); return value.split(', ').map(x => fromString(d._decoder, x));
} }
// if (d instanceof t.Tuple) {
// return value.split(', ').map((x, idx) => fromString(d._tuple[idx], x));
// }
// if (d instanceof t.Literals) {
// // TODO: проверять значение исходя из типа rep._literals
// return value;
// }
if (d instanceof t.Dict) { if (d instanceof t.Dict) {
try { try {
return JSON.parse(value); return JSON.parse(value);
...@@ -141,6 +132,9 @@ export function fromString(d: Decoder<any>, value: string): unknown { ...@@ -141,6 +132,9 @@ export function fromString(d: Decoder<any>, value: string): unknown {
if (d instanceof WithDefault) { if (d instanceof WithDefault) {
return fromString(d._decoder, value); return fromString(d._decoder, value);
} }
if (d instanceof t.Variants) {
return value;
}
return fromString(d.toDecoder(), value); return fromString(d.toDecoder(), value);
} }
...@@ -225,6 +219,10 @@ export function configInfo(decoder: RecordDecoder<any>, prefix=''): Record<strin ...@@ -225,6 +219,10 @@ export function configInfo(decoder: RecordDecoder<any>, prefix=''): Record<strin
info.default = d._default; info.default = d._default;
return info; return info;
} }
if (d instanceof t.Variants) {
return { type: d._variants.map(x => JSON.stringify(x)).join('|') };
}
const info = go(d.toDecoder()); const info = go(d.toDecoder());
if (d instanceof ConfigDescription) { if (d instanceof ConfigDescription) {
......
...@@ -20,6 +20,7 @@ export type Decoder<A> = ...@@ -20,6 +20,7 @@ export type Decoder<A> =
| OneOf<A> | OneOf<A>
| Discriminate<A> | Discriminate<A>
| ToDecoder<A> | ToDecoder<A>
;
/** /**
...@@ -27,6 +28,7 @@ export type Decoder<A> = ...@@ -27,6 +28,7 @@ export type Decoder<A> =
*/ */
export type Problem = { export type Problem = {
value: any; value: any;
path: string[];
rootValue: any; rootValue: any;
decoder: Decoder<any>; decoder: Decoder<any>;
rootDecoder: Decoder<any>; rootDecoder: Decoder<any>;
...@@ -51,6 +53,7 @@ export class DecoderBase<A> { ...@@ -51,6 +53,7 @@ export class DecoderBase<A> {
readonly _A: A; readonly _A: A;
static rootDecoder: Decoder<any>|undefined = undefined; static rootDecoder: Decoder<any>|undefined = undefined;
static rootValue: any = undefined; static rootValue: any = undefined;
static path: string[] = [];
/** /**
* Печать декодера в виде выражения которым он был создан * Печать декодера в виде выражения которым он был создан
...@@ -161,6 +164,15 @@ export class RecordDecoder<A> extends DecoderBase<A> { ...@@ -161,6 +164,15 @@ export class RecordDecoder<A> extends DecoderBase<A> {
extend<F extends { [K: string]: Decoder<any> }>(fields: F): RecordDecoder<A & { [K in keyof F]: F[K]['_A'] }> { extend<F extends { [K: string]: Decoder<any> }>(fields: F): RecordDecoder<A & { [K in keyof F]: F[K]['_A'] }> {
return new RecordDecoder({ ...this._description, ...fields as any }); return new RecordDecoder({ ...this._description, ...fields as any });
} }
pick<K extends keyof A>(...keys: K[]): RecordDecoder<Pick<A, K>> {
return new RecordDecoder(keys.reduce<any>((acc, k) => (acc[k] = this._description[k as string], acc), {}));
}
omit<K extends keyof A>(...keys: K[]): RecordDecoder<Omit<A, K>> {
const description = Object.keys(this._description).reduce((acc, k) => (keys.indexOf(k as any) === -1 && (acc[k] = this._description[k]), acc), {});
return new RecordDecoder(description);
}
} }
export class AtDecoder<A> extends DecoderBase<A> { export class AtDecoder<A> extends DecoderBase<A> {
...@@ -221,6 +233,16 @@ export class WithDefault<A> extends ToDecoder<A> { ...@@ -221,6 +233,16 @@ export class WithDefault<A> extends ToDecoder<A> {
} }
} }
export class Variants<A> extends ToDecoder<A> {
constructor(
readonly _variants: A[],
) { super(); }
toDecoder() {
return new OneOf<A>(this._variants.map(x => decoder(v => v === x ? Either.of(v) : Either.failure(`expected ${x}, got ${fancyTypeOf(v)}`))));
}
}
/** /**
* Результат валидаии * Результат валидаии
...@@ -381,13 +403,15 @@ export function optional(key: string|string[], dec: Decoder<any>, def: any): Dec ...@@ -381,13 +403,15 @@ export function optional(key: string|string[], dec: Decoder<any>, def: any): Dec
/** /**
* Создание декодера перечислением всех допустимых значений * Создание декодера перечислением всех допустимых значений
*/ */
export function literals<A extends Expr[]>(...array: A): Decoder<A[number]>; export function variants<A extends Expr[]>(...array: A): Variants<A[number]>;
export function literals<A extends Expr[]>(array: A): Decoder<A[number]>; export function variants<A extends Expr[]>(array: A): Variants<A[number]>;
export function literals(): Decoder<any> { export function variants(): Variants<any> {
const literals: Expr[] = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments); return new Variants(Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments));
return new OneOf(literals.map(x => decoder(v => v === x ? Either.of(v) : Either.failure(`expected ${x}, got ${fancyTypeOf(v)}`))));
} }
// @deprecated
export const literals = variants;
/** /**
* Кортежи разных размеров. Проверяемое значение необязательно должно * Кортежи разных размеров. Проверяемое значение необязательно должно
...@@ -414,7 +438,7 @@ export function printProblems(problems: Problem): void { ...@@ -414,7 +438,7 @@ export function printProblems(problems: Problem): void {
console.log('%croot decoder:', 'font-weight: 600;', problems.rootDecoder.prettyPrint()); console.log('%croot decoder:', 'font-weight: 600;', problems.rootDecoder.prettyPrint());
console.log('%cfailed value:', 'font-weight: 600;', problems.value); console.log('%cfailed value:', 'font-weight: 600;', problems.value);
console.log('%croot value:', 'font-weight: 600;', problems.rootValue); console.log('%croot value:', 'font-weight: 600;', problems.rootValue);
//console.log('decoders ', problems.decoders.map(x => x.prettyPrint())); console.log('%cproblem path:', 'font-weight: 600;', ['_', ...problems.path].join('.'));
} }
...@@ -428,20 +452,29 @@ function fancyTypeOf(value: any): string { ...@@ -428,20 +452,29 @@ function fancyTypeOf(value: any): string {
* Выполнение валидации * Выполнение валидации
*/ */
export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Problem, A> { export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Problem, A> {
const pathLength = DecoderBase.path.length;
const cleanup = () => DecoderBase.path.splice(pathLength);
if (decoder instanceof Custom) { if (decoder instanceof Custom) {
return decoder._validate(value).mapLeft(projectProblem); DecoderBase.path.push('<custom>');
const output = decoder._validate(value).mapLeft(projectProblem);
cleanup();
return output;
} }
if (decoder instanceof ArrayDecoder) { if (decoder instanceof ArrayDecoder) {
const output: any[] = []; const output: any[] = [];
if (!Array.isArray(value)) return Either.failure(projectProblem('not an array')); if (!Array.isArray(value)) return Either.failure(projectProblem('not an array'));
DecoderBase.path.push('0');
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
DecoderBase.path[DecoderBase.path.length - 1] = String(i);
const ethr = doValidate(decoder._decoder, value[i]); const ethr = doValidate(decoder._decoder, value[i]);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': { return ethr; } case 'Left': { return ethr; }
case 'Right': output.push(ethr.value); break; case 'Right': output.push(ethr.value); break;
} }
} }
cleanup();
return Either.of(output as any as A); return Either.of(output as any as A);
} }
...@@ -449,14 +482,17 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -449,14 +482,17 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
if (typeof (value) !== 'object') return Either.failure(projectProblem('not an object')); if (typeof (value) !== 'object') return Either.failure(projectProblem('not an object'));
if (value === null) return Either.failure(projectProblem('found null')); if (value === null) return Either.failure(projectProblem('found null'));
const output: { [k: string]: A } = {}; const output: { [k: string]: A } = {};
DecoderBase.path.push('');
for (let key in value) { for (let key in value) {
if (!value.hasOwnProperty(key)) continue; if (!value.hasOwnProperty(key)) continue;
DecoderBase.path[DecoderBase.path.length - 1] = key;
const ethr = doValidate(decoder._decoder, value[key]); const ethr = doValidate(decoder._decoder, value[key]);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': { return ethr; } case 'Left': { return ethr; }
case 'Right': output[key] = ethr.value; break; case 'Right': output[key] = ethr.value; break;
} }
} }
cleanup();
return Either.of(output as any as A); return Either.of(output as any as A);
} }
...@@ -464,7 +500,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -464,7 +500,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
if (typeof (value) !== 'object') return Either.failure(projectProblem('not an object')); if (typeof (value) !== 'object') return Either.failure(projectProblem('not an object'));
if (value === null) return Either.failure(projectProblem('found null')); if (value === null) return Either.failure(projectProblem('found null'));
const output: { [k: string]: any } = {}; const output: { [k: string]: any } = {};
DecoderBase.path.push('');
for (let key in decoder._description) { for (let key in decoder._description) {
DecoderBase.path[DecoderBase.path.length - 1] = key;
if (!decoder._description.hasOwnProperty(key)) continue; if (!decoder._description.hasOwnProperty(key)) continue;
const ethr = doValidate(decoder._description[key], value[key]); const ethr = doValidate(decoder._description[key], value[key]);
switch(ethr.tag) { switch(ethr.tag) {
...@@ -472,11 +510,13 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -472,11 +510,13 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
case 'Right': output[key] = ethr.value; break; case 'Right': output[key] = ethr.value; break;
} }
} }
cleanup();
return Either.of(output as any as A); return Either.of(output as any as A);
} }
if (decoder instanceof AtDecoder) { if (decoder instanceof AtDecoder) {
let iter = value as any; let iter = value as any;
decoder._path.forEach(k => DecoderBase.path.push(k + ''));
for (let i in decoder._path) { for (let i in decoder._path) {
if (iter === undefined || !iter.hasOwnProperty(decoder._path[i])) { if (iter === undefined || !iter.hasOwnProperty(decoder._path[i])) {
iter = undefined; iter = undefined;
...@@ -484,6 +524,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -484,6 +524,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
} }
iter = iter[decoder._path[i]]; iter = iter[decoder._path[i]];
} }
cleanup();
return doValidate(decoder._decoder, iter); return doValidate(decoder._decoder, iter);
} }
...@@ -517,7 +558,10 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -517,7 +558,10 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
} }
if (decoder instanceof Chain) { if (decoder instanceof Chain) {
return doValidate(decoder._then(doValidate(decoder._decoder, value)), value); DecoderBase.path.push('<chain>');
const output = doValidate(decoder._then(doValidate(decoder._decoder, value)), value);
cleanup();
return output;
} }
if (decoder instanceof Discriminate) { if (decoder instanceof Discriminate) {
...@@ -526,7 +570,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -526,7 +570,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
if (typeof(value) !== 'object' || !value) return Either.failure(projectProblem('expected an object')); if (typeof(value) !== 'object' || !value) return Either.failure(projectProblem('expected an object'));
if (!value.hasOwnProperty(key)) return Either.failure(projectProblem(`expected input would have key '${key}'`)); if (!value.hasOwnProperty(key)) return Either.failure(projectProblem(`expected input would have key '${key}'`));
if (!(value[key] in record)) return Either.failure(projectProblem(`unknown value of discriminated key ${value[key]}`)); if (!(value[key] in record)) return Either.failure(projectProblem(`unknown value of discriminated key ${value[key]}`));
return record[value[key]].validate(value).map(x => ({ ...x, [key]: value[key] })); const output = record[value[key]].validate(value).map(x => ({ ...x, [key]: value[key] }));
cleanup();
return output;
} }
if (decoder instanceof ToDecoder) { if (decoder instanceof ToDecoder) {
...@@ -537,7 +583,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -537,7 +583,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
function projectProblem(problem: Problem|string): Problem { function projectProblem(problem: Problem|string): Problem {
if (typeof(problem) !== 'string') return problem; if (typeof(problem) !== 'string') return problem;
return { value, rootValue: DecoderBase.rootValue, decoder, rootDecoder: DecoderBase.rootDecoder || decoder, message: problem, }; return { value, rootValue: DecoderBase.rootValue, decoder, rootDecoder: DecoderBase.rootDecoder || decoder, message: problem, path: DecoderBase.path };
} }
} }
...@@ -559,6 +605,7 @@ export function prettyPrint(decoder: Decoder<any>): string { ...@@ -559,6 +605,7 @@ export function prettyPrint(decoder: Decoder<any>): string {
if (decoder instanceof Pure) return `t.of(${JSON.stringify(decoder._value)})`; if (decoder instanceof Pure) return `t.of(${JSON.stringify(decoder._value)})`;
if (decoder instanceof Chain) return `${decoder.prettyPrint()}.chain(<func>)`; if (decoder instanceof Chain) return `${decoder.prettyPrint()}.chain(<func>)`;
if (decoder instanceof OneOf) return `t.oneOf(${decoder._alternatives.map(x => x.prettyPrint()).join(', ')})`; if (decoder instanceof OneOf) return `t.oneOf(${decoder._alternatives.map(x => x.prettyPrint()).join(', ')})`;
if (decoder instanceof Variants) return `t.literals(${decoder._variants.map(x => JSON.stringify(x)).join(', ')})`;
if (decoder instanceof Discriminate) { if (decoder instanceof Discriminate) {
const discriminator = JSON.stringify(decoder._discriminator); const discriminator = JSON.stringify(decoder._discriminator);
const alternatives = Object.keys(decoder._alternatives).map(k => JSON.stringify(k) + ": " + decoder._alternatives[k].prettyPrint()).join(', '); const alternatives = Object.keys(decoder._alternatives).map(k => JSON.stringify(k) + ": " + decoder._alternatives[k].prettyPrint()).join(', ');
...@@ -570,3 +617,7 @@ export function prettyPrint(decoder: Decoder<any>): string { ...@@ -570,3 +617,7 @@ export function prettyPrint(decoder: Decoder<any>): string {
return absurd(decoder); return absurd(decoder);
} }
// Utilies types based on
// https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-307871458
export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment