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 = {
// Базовый класс для наследования методов
export class DecoderBase<A> {
readonly _A: A;
static rootDecoder: Decoder<any>;
static rootDecoder: Decoder<any>|undefined = undefined;
static rootValue: any = undefined;
/**
......@@ -120,7 +120,7 @@ export class DecoderBase<A> {
withDefault(defValue: A): Decoder<A>;
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> {
......@@ -254,19 +254,23 @@ export class DiscriminatedDecoder<A> extends DecoderBase<A> {
/**
* `discriminateOn` комбинатор
*/
export class WithDefaultDecoder<A> extends DecoderBase<A> {
constructor(
readonly decoder: Decoder<A>,
readonly defaultValue: A,
) { super(); }
export abstract class HasDecoder<A> extends DecoderBase<A> {
abstract toDecoder(): Decoder<A>;
}
/**
* `discriminateOn` комбинатор
*/
export abstract class HasDecoder<A> extends DecoderBase<A> {
abstract toDecoder(): Decoder<A>;
export class WithDefaultDecoder<A> extends HasDecoder<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 {
}
// Хелпер
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
const output: any[] = [];
if (!Array.isArray(value)) return Either.failure(projectProblem('not an array'));
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) {
case 'Left': { ethr.value.path.push(i); return ethr; }
case 'Left': { return ethr; }
case 'Right': output.push(ethr.value); break;
}
}
......@@ -528,14 +521,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
}
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 (value === null) return Either.failure(projectProblem('found null'));
const output: { [k: string]: A } = {};
for (let key in value) {
if (!value.hasOwnProperty(key)) continue;
const ethr = this.decoder.validate(value[key]);
const ethr = doValidate(decoder.decoder, value[key]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; }
case 'Left': { return ethr; }
case 'Right': output[key] = ethr.value; break;
}
}
......@@ -543,14 +536,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
}
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 (value === null) return Either.failure(projectProblem('found null'));
const output: { [k: string]: any } = {};
for (let key in this.description) {
if (!this.description.hasOwnProperty(key)) continue;
const ethr = this.description[key].validate(value[key]);
for (let key in decoder.description) {
if (!decoder.description.hasOwnProperty(key)) continue;
const ethr = doValidate(decoder.description[key], value[key]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; }
case 'Left': { return ethr; }
case 'Right': output[key] = ethr.value; break;
}
}
......@@ -559,14 +552,14 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
if (decoder instanceof AtDecoder) {
let iter = value as any;
for (let i in this.path) {
if (iter === undefined || !iter.hasOwnProperty(this.path[i])) {
for (let i in decoder.path) {
if (iter === undefined || !iter.hasOwnProperty(decoder.path[i])) {
iter = undefined;
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) {
......@@ -588,8 +581,8 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
}
if (decoder instanceof OneOfDecoder) {
for (const decoder of this.alternatives) {
const ethr = decoder.validate(value);
for (const d of decoder.alternatives) {
const ethr = doValidate(d, value);
switch(ethr.tag) {
case 'Left': break;
case 'Right': return ethr;
......@@ -619,7 +612,7 @@ export function doValidate<A>(decoder: Decoder<A>, value: unknown): Either<Probl
function projectProblem(problem: Problem|string): 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