Commit e8bdeb08 by Vladislav Lagunov

Добавлен ~/config-reader

parent 6369d976
import * as t from '../decoder';
import { camelCase } from 'lodash';
import { absurd } from '../types';
/**
* Источник данных для чтения конфигов
*/
export type ConfigSource =
| { tag: 'Cli', args: string[], prefix: string }
| { tag: 'Location/search', value: string, transformKey(x: string): string }
| { tag: 'JSON', value: object }
| { tag: 'Concat', sources: ConfigSource[] }
export function cli(args: string[], prefix: string = ''): ConfigSource {
return { tag: 'Cli', args, prefix };
}
export function queryString(value: string, transformKey = camelCase): ConfigSource {
return { tag: 'Location/search', value, transformKey };
}
export function json(value: object): ConfigSource {
return { tag: 'JSON', value };
}
export function concat(...sources: ConfigSource[]): ConfigSource {
return { tag: 'Concat', sources };
}
export function validate<A>(config: ConfigSource, decoder: t.RecordDecoder<A>) {
return decoder.validate(merge(decoder, config));
}
export function merge<A>(decoder: t.RecordDecoder<A>, ...srcs: ConfigSource[]): object {
const source = srcs.length === 1 ? srcs[0] : concat(...srcs);
if (source.tag === 'Cli') {
const value: Record<string, any> = {};
for (let i = 0; i < source.args.length; i++) {
const prefixLen = '--'.length + source.prefix.length;
const rest = source.args[i].substr(prefixLen);
const [k, v] = rest.indexOf('=') === -1 ? [rest, source.args[++i] || ''] : source.args[i].split('=');
// Проверка того что это известно
if (!decoder.description.hasOwnProperty(k)) continue;
value[k] = decoderFromString(decoder.description[k], v);
}
return value;
}
if (source.tag === 'Location/search') {
const substring = source.value[0] === '?' ? source.value.substr(1) : source.value;
const value = substring.split('&').map(x => x.split('=')).reduce((acc, [k, v]: any) => {
const key = source.transformKey(decodeURIComponent(k));
if (!decoder.description.hasOwnProperty(key)) return acc;
acc[key] = decoderFromString(decoder.description[key], decodeURIComponent(v));
return acc;
}, {});
return value;
}
if (source.tag === 'JSON') {
return source.value;
}
if (source.tag === 'Concat' ) {
// @ts-ignore
return Object.assign({}, ...source.sources.map(x => merge(decoder, x)));
}
return absurd(source);
}
export function decoderFromString(decoder: t.Decoder<any>, value: string): any {
if (decoder instanceof t.CustomDecoder) {
throw new Error('[config-reader] CustomDecoder is not supported');
}
if (decoder instanceof t.ArrayDecoder) {
return value.split(', ').map(x => decoderFromString(decoder.decoder, x));
}
if (decoder instanceof t.DictionaryDecoder) {
try {
return JSON.parse(value);
} catch(e) {
return value;
}
}
if (decoder instanceof t.RecordDecoder) {
try {
return JSON.parse(value);
} catch(e) {
return value;
}
}
if (decoder instanceof t.AtDecoder) {
try {
return JSON.parse(value);
} catch(e) {
return value;
}
}
if (decoder instanceof t.PrimitiveDecoder) {
if (decoder.primitive === 'string') return value;
if (decoder.primitive === 'boolean') return !(value === 'false' || value === '0');
if (decoder.primitive === 'undefined') return undefined;
if (decoder.primitive === 'null') return null;
if (decoder.primitive === 'any') return value;
if (decoder.primitive === 'nat') return Number(value);
if (decoder.primitive === 'int') return Number(value);
if (decoder.primitive === 'float') return Number(value);
return absurd(decoder.primitive);
}
if (decoder instanceof t.PureDecoder) {
return value;
}
if (decoder instanceof t.ChainDecoder) {
throw new Error('[config-reader] ChainDecoder is not supported');
}
if (decoder instanceof t.OneOfDecoder) {
throw new Error('[config-reader] OneOfDecoder is not supported');
}
if (decoder instanceof t.DiscriminatedDecoder) {
try {
return JSON.parse(value);
} catch(e) {
return value;
}
}
if (decoder instanceof t.HasDecoder) {
if (decoder instanceof t.WithDefaultDecoder) {
return decoderFromString(decoder.decoder, value);
}
return decoderFromString(decoder.toDecoder(), value);
}
return absurd(decoder);
}
...@@ -49,7 +49,7 @@ const defaultValidateOptions: ValidateOptions = { ...@@ -49,7 +49,7 @@ const defaultValidateOptions: ValidateOptions = {
// Базовый класс для наследования методов // Базовый класс для наследования методов
export class DecoderBase<A> { export class DecoderBase<A> {
readonly _A: A; readonly _A: A;
static rootDecoder: Decoder<any>; static rootDecoder: Decoder<any>|undefined = undefined;
static rootValue: any = undefined; static rootValue: any = undefined;
/** /**
...@@ -120,7 +120,7 @@ export class DecoderBase<A> { ...@@ -120,7 +120,7 @@ export class DecoderBase<A> {
withDefault(defValue: A): Decoder<A>; withDefault(defValue: A): Decoder<A>;
withDefault<B extends Expr>(defValue: B): Decoder<A|B>; withDefault<B extends Expr>(defValue: B): Decoder<A|B>;
withDefault<B extends Expr>(defValue: B): Decoder<A|B> { withDefault<B extends Expr>(defValue: B): Decoder<A|B> {
return new OneOfDecoder([this as any, of(defValue)]); return new WithDefaultDecoder(this as any, defValue);
} }
refine(pred: (a: A) => boolean): Decoder<A> { refine(pred: (a: A) => boolean): Decoder<A> {
...@@ -254,19 +254,23 @@ export class DiscriminatedDecoder<A> extends DecoderBase<A> { ...@@ -254,19 +254,23 @@ export class DiscriminatedDecoder<A> extends DecoderBase<A> {
/** /**
* `discriminateOn` комбинатор * `discriminateOn` комбинатор
*/ */
export class WithDefaultDecoder<A> extends DecoderBase<A> { export abstract class HasDecoder<A> extends DecoderBase<A> {
constructor( abstract toDecoder(): Decoder<A>;
readonly decoder: Decoder<A>,
readonly defaultValue: A,
) { super(); }
} }
/** /**
* `discriminateOn` комбинатор * `discriminateOn` комбинатор
*/ */
export abstract class HasDecoder<A> extends DecoderBase<A> { export class WithDefaultDecoder<A> extends HasDecoder<A> {
abstract toDecoder(): Decoder<A>; constructor(
readonly decoder: Decoder<A>,
readonly defaultValue: A,
) { super(); }
toDecoder() {
return new OneOfDecoder<A>([this.decoder, of(this.defaultValue)]);
}
} }
...@@ -495,17 +499,6 @@ function fancyTypeOf(value: any): string { ...@@ -495,17 +499,6 @@ function fancyTypeOf(value: any): string {
} }
// Хелпер
function prettyPrintPath(path: Array<string|number>): string {
let output = '_';
for (let i = path.length - 1; i >= 0; i--) {
if (/^[a-zA-Z_$][\w_$]*$/.test('' + path[i])) output += '.' + path[i];
else output += `[${JSON.stringify(path[i])}]`;
}
return output;
}
/** /**
* Выполнение валидации * Выполнение валидации
*/ */
...@@ -518,9 +511,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -518,9 +511,9 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
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'));
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
const ethr = this.decoder.validate(value[i]); const ethr = doValidate(decoder.decoder, value[i]);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': { ethr.value.path.push(i); return ethr; } case 'Left': { return ethr; }
case 'Right': output.push(ethr.value); break; case 'Right': output.push(ethr.value); break;
} }
} }
...@@ -528,14 +521,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -528,14 +521,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
} }
if (decoder instanceof DictionaryDecoder) { if (decoder instanceof DictionaryDecoder) {
if (value === null) return Either.failure(projectProblem('found null'));
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'));
const output: { [k: string]: A } = {}; const output: { [k: string]: A } = {};
for (let key in value) { for (let key in value) {
if (!value.hasOwnProperty(key)) continue; if (!value.hasOwnProperty(key)) continue;
const ethr = this.decoder.validate(value[key]); const ethr = doValidate(decoder.decoder, value[key]);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; } case 'Left': { return ethr; }
case 'Right': output[key] = ethr.value; break; case 'Right': output[key] = ethr.value; break;
} }
} }
...@@ -543,14 +536,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -543,14 +536,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
} }
if (decoder instanceof RecordDecoder) { if (decoder instanceof RecordDecoder) {
if (value === null) return Either.failure(projectProblem('found null'));
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'));
const output: { [k: string]: any } = {}; const output: { [k: string]: any } = {};
for (let key in this.description) { for (let key in decoder.description) {
if (!this.description.hasOwnProperty(key)) continue; if (!decoder.description.hasOwnProperty(key)) continue;
const ethr = this.description[key].validate(value[key]); const ethr = doValidate(decoder.description[key], value[key]);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; } case 'Left': { return ethr; }
case 'Right': output[key] = ethr.value; break; case 'Right': output[key] = ethr.value; break;
} }
} }
...@@ -559,14 +552,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -559,14 +552,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
if (decoder instanceof AtDecoder) { if (decoder instanceof AtDecoder) {
let iter = value as any; let iter = value as any;
for (let i in this.path) { for (let i in decoder.path) {
if (iter === undefined || !iter.hasOwnProperty(this.path[i])) { if (iter === undefined || !iter.hasOwnProperty(decoder.path[i])) {
iter = undefined; iter = undefined;
break; break;
} }
iter = iter[this.path[i]]; iter = iter[decoder.path[i]];
} }
return this.decoder.validate(iter); return doValidate(decoder.decoder, iter);
} }
if (decoder instanceof PrimitiveDecoder) { if (decoder instanceof PrimitiveDecoder) {
...@@ -588,8 +581,8 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -588,8 +581,8 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
} }
if (decoder instanceof OneOfDecoder) { if (decoder instanceof OneOfDecoder) {
for (const decoder of this.alternatives) { for (const d of decoder.alternatives) {
const ethr = decoder.validate(value); const ethr = doValidate(d, value);
switch(ethr.tag) { switch(ethr.tag) {
case 'Left': break; case 'Left': break;
case 'Right': return ethr; case 'Right': return ethr;
...@@ -619,7 +612,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl ...@@ -619,7 +612,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, message: problem, }; return { value, rootValue: DecoderBase.rootValue, decoder, rootDecoder: DecoderBase.rootDecoder || decoder, message: problem, };
} }
} }
......
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