Commit 42697ecc by Vladislav Lagunoff

Добавлены модули из @bitmaster/core

parent 0fd99798
import { Either } from '~/either';
import { Expr, absurd } from '~/types';
// This library helps you declaratively construct type-safe validators for untyped JSON
// inspired by http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode
// and https://github.com/gcanti/io-ts
/** ADT */
export type Decoder<A> =
| CustomDecoder<A>
| ArrayDecoder<A>
| DictionaryDecoder<A>
| RecordDecoder<A>
| AtDecoder<A>
| PrimitiveDecoder<A>
| PureDecoder<A>
| ChainDecoder<A>
| OneOfDecoder<A>
| DiscriminateOnDecoder<A>
/**
* Информация о возникшей ошибке
*/
export type Problem = {
value: any;
rootValue: any;
decoders: Decoder<any>[];
message: string;
path: Array<string|number>;
}
/**
* Опции для `DecoderBase.prototype.validate`
*/
export interface ValidateOptions {
printProblem: boolean;
}
const defaultValidateOptions: ValidateOptions = {
printProblem: false,
};
// Базовый класс для наследования методов
export class DecoderBase<A> {
readonly _A: A;
private static path: Decoder<any>[] = [];
private static rootValue: any = undefined;
/**
* Печать декодера в виде выражения которым он был создан
*/
prettyPrint(): string {
const self = this as any as Decoder<A>;
}
/**
* Валидация произвольного значения
*/
validate(value: any, options?: Partial<ValidateOptions>): Either<Problem, A> {
const self = this as any as Decoder<A>;
let _options: ValidateOptions;
let cleanUpRoot = false;
DecoderBase.path.push(self);
if (DecoderBase.rootValue === undefined) { DecoderBase.rootValue = value; cleanUpRoot = true; }
const output = doValidate(self, value)
DecoderBase.path.pop();
if (cleanUpRoot) DecoderBase.rootValue = undefined;
// Печать проблемы в консоль если выставлена соответствующая опция
if (output.tag === 'Left' && makeOptions().printProblem) {
printProblems(output.value);
}
return output;
function makeOptions(): ValidateOptions {
if (_options) return _options;
return _options = options ? { ...defaultValidateOptions, ...options } : defaultValidateOptions;
}
function doValidate(decoder: Decoder<A>, value: any): Either<Problem, A> {
switch(decoder.tag) {
case 'CustomDecoder': return decoder.validateCustom(value).mapLeft(decorateProblem);;
case 'ArrayDecoder': return decoder.validateArray(value).mapLeft(decorateProblem);;
case 'DictionaryDecoder': return decoder.validateDictionary(value).mapLeft(decorateProblem);;
case 'RecordDecoder': return decoder.validateRecord(value).mapLeft(decorateProblem);;
case 'AtDecoder': return decoder.validateAt(value).mapLeft(decorateProblem);;
case 'PrimitiveDecoder': return decoder.validatePrimitive(value).mapLeft(decorateProblem);;
case 'PureDecoder': return decoder.value.mapLeft(decorateProblem);;
case 'OneOfDecoder': return decoder.validateOneOf(value).mapLeft(decorateProblem);;
case 'ChainDecoder': return decoder.andThen(decoder.decoder.validate(value)).validate(value);;
}
}
function decorateProblem(problem: Problem|string): Problem {
return typeof(problem) === 'string'
? { value, rootValue: DecoderBase.rootValue, decoders: DecoderBase.path.slice(), message: problem, path: [] }
: problem;
}
}
map<B>(f: (a: A) => B): Decoder<B> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(fail, x => of(f(x))));
}
mapTo<B>(value: B): Decoder<B> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(fail, () => of(value)));
}
chain<B>(f: (a: A) => Decoder<B>): Decoder<B> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(fail, f));
}
chainTo<B>(value: Decoder<B>): Decoder<B> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(fail, () => value));
}
mapProblem(f: (a: Problem) => Problem): Decoder<A> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(f, x => x));
}
mapProblemTo(value: Problem): Decoder<A> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, x => x.fold(() => value, x => x));
}
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)]);
}
refine(pred: (a: A) => boolean): Decoder<A> {
const self = this as any as Decoder<A>;
return new ChainDecoder(self, ethr => {
switch (ethr.tag) {
case 'Left': return new PureDecoder(ethr);
case 'Right': return pred(ethr.value) ? new PureDecoder(ethr) : fail('refinement failed');
}
});
}
}
/**
* Декодер с произвольной функцией для валидации
*/
export class CustomDecoder<A> extends DecoderBase<A> {
readonly tag: 'CustomDecoder' = 'CustomDecoder';
constructor(
readonly name: string,
readonly validateCustom: (val: any) => Either<Problem|string, A>,
) { super(); }
}
/**
* Декодер массивов
*/
export class ArrayDecoder<A> extends DecoderBase<A> {
readonly tag: 'ArrayDecoder' = 'ArrayDecoder';
constructor(
readonly decoder: Decoder<any>,
) { super(); }
validateArray(value: any): Either<Problem|string, A> {
const output: any[] = [];
if (!Array.isArray(value)) return Either.failure('not an array');
for (let i = 0; i < value.length; i++) {
const ethr = this.decoder.validate(value[i]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(i); return ethr; }
case 'Right': output.push(ethr.value); break;
}
}
return Either.of(output as any as A);
}
}
/**
* Декодер словарей (или хешей, те объектов с произвольным кол-вом ключей)
*/
export class DictionaryDecoder<A> extends DecoderBase<A> {
readonly tag: 'DictionaryDecoder' = 'DictionaryDecoder';
constructor(
readonly decoder: Decoder<any>,
) { super(); }
validateDictionary(value: any): Either<Problem|string, A> {
if (value === null) return Either.failure('found null');
if (typeof (value) !== 'object') return Either.failure('not an object');
const output: { [k: string]: A } = {};
for (let key in value) {
if (!value.hasOwnProperty(key)) continue;
const ethr = this.decoder.validate(value[key]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; }
case 'Right': output[key] = ethr.value; break;
}
}
return Either.of(output as any as A);
}
}
/**
* Декодер записей (объекты с фиксированным количеством полей)
*/
export class RecordDecoder<A> extends DecoderBase<A> {
readonly tag: 'RecordDecoder' = 'RecordDecoder';
constructor(
readonly description: Record<string, Decoder<any>>,
) { super(); }
validateRecord(value: any): Either<Problem|string, A> {
if (value === null) return Either.failure('found null');
if (typeof (value) !== 'object') return Either.failure('not an object');
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]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return ethr; }
case 'Right': output[key] = ethr.value; break;
}
}
return Either.of(output as any as A);
}
}
/**
* Декодер поля с указанным индексом
*/
export class AtDecoder<A> extends DecoderBase<A> {
readonly tag: 'AtDecoder' = 'AtDecoder';
constructor(
readonly path: Array<string|number>,
readonly decoder: Decoder<A>,
) { super(); }
validateAt(value: any): Either<Problem|string, A> {
let iter = value;
for (let i in this.path) {
if (iter === undefined || !iter.hasOwnProperty(this.path[i])) {
iter = undefined;
break;
}
iter = iter[this.path[i]];
}
return this.decoder.validate(iter);
}
}
/**
* Декодер для примитовов
*/
export class PrimitiveDecoder<A> extends DecoderBase<A> {
readonly tag: 'PrimitiveDecoder' = 'PrimitiveDecoder';
constructor(
readonly primitive: 'null'|'undefined'|'string'|'boolean'|'any'|'nat'|'int'|'float'
) { super(); }
validatePrimitive(value: any): Either<string, A> {
switch (this.primitive) {
case 'null': return value === null ? Either.of(value) : Either.failure(`expected null, got ${fancyTypeOf(value)}`);
case 'undefined': return value === undefined ? Either.of(value) : Either.failure(`expected undefined, got ${fancyTypeOf(value)}`);
case 'string': return typeof(value) === 'string' ? Either.of(value as any) : Either.failure(`expected a string, got ${fancyTypeOf(value)}`);
case 'boolean': return typeof(value) === 'boolean' ? Either.of(value as any) : Either.failure(`expected a boolean, got ${fancyTypeOf(value)}`);
case 'any': return Either.of(value);
case 'nat': return typeof (value) !== 'number' ? Either.failure('not a number') : (value|0) === value && value >= 0 ? Either.of(value as any) : Either.failure('not a natural number');
case 'int': return typeof (value) !== 'number' ? Either.failure('not a number') : (value|0) === value ? Either.of(value as any) : Either.failure('not an integer')
case 'float': return typeof (value) !== 'number' ? Either.failure('not a number') : Either.of(value as any);
}
}
}
/**
* Тривиальный декодер
*/
export class PureDecoder<A> extends DecoderBase<A> {
constructor(
readonly value: Either<Problem|string, A>,
) { super(); }
}
/**
* Монадный комбинатор
*/
export class ChainDecoder<A> extends DecoderBase<A> {
constructor(
readonly decoder: Decoder<any>,
readonly andThen: (x: Validation<any>) => Decoder<A>,
) { super(); }
}
/**
* `oneOf` комбинатор
*/
export class OneOfDecoder<A> extends DecoderBase<A> {
constructor(
readonly alternatives: Decoder<any>[],
) { super(); }
validateOneOf(value: any): Either<Problem|string, A> {
for (const decoder of this.alternatives) {
const ethr = decoder.validate(value);
switch(ethr.tag) {
case 'Left': break;
case 'Right': return ethr;
}
}
return Either.failure('none of decoders succeded');
}
}
/**
* `discriminateOn` комбинатор
*/
export class DiscriminateOnDecoder<A> extends DecoderBase<A> {
constructor(
readonly discriminator: string|number,
readonly alternatives: Record<string|number, Decoder<unknown>>,
) { super(); }
}
/**
* `discriminateOn` комбинатор
*/
export class WithDefaultDecoder<A> extends DecoderBase<A> {
constructor(
readonly decoder: Decoder<A>,
readonly defaultValue: A,
) { super(); }
}
/**
* Тип результата для функции-валидатора
*/
export type Validation<A> = Either<Problem|string, A>;
/**
* Конструктир кастомных декодеров
*/
export function decoder<A>(validate: (value: any) => Either<Problem|string, A>): CustomDecoder<A>;
export function decoder<A>(name: string, validate: (value: any) => Either<Problem|string, A>): CustomDecoder<A>;
export function decoder(): any {
if (arguments.length === 1) return new CustomDecoder('custom', arguments[0]);
if (arguments.length === 2) return new CustomDecoder(arguments[0], arguments[1]);
throw new TypeError(`decoder: invalid number of arguments`);
}
/**
* Алиас для `x => new PureDecoder(Either.of(x))`
*/
export function of<A extends Expr>(a: A): PureDecoder<A> {
return new PureDecoder(Either.of(a));
}
/**
* Алиас для `x => new PureDecoder(Either.failure(x))`
*/
export function fail(x: Problem|string): Decoder<never> {
return new PureDecoder(Either.failure(x));
}
/**
* Аппликативный комбинатор
* TODO: сделать отдельным вариантом в ADT для реализации `prettyPrint`
*/
export function ap<A,B>(a: Decoder<A>, f:(a: A) => B): CustomDecoder<B>;
export function ap<A,B,C>(a: Decoder<A>, b: Decoder<B>, f:(a: A, b: B) => C):CustomDecoder<C>;
export function ap<A,B,C,D>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, f:(a: A, b: B, c: C) => D):CustomDecoder<D>;
export function ap<A,B,C,D,E>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, f:(a: A, b: B, c: C, d: D) => E):CustomDecoder<E>;
export function ap<A,B,C,D,E,F>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f:(a: A, b: B, c: C, d: D, e: E) => F):CustomDecoder<F>;
export function ap<A,B,C,D,E,F,G>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f_:Decoder<F>, f:(a: A, b: B, c: C, d: D, e: E, f: F) => G):CustomDecoder<G>;
export function ap<A,B,C,D,E,F,G,H>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f_:Decoder<F>, g: Decoder<G>, f:(a: A, b: B, c: C, d: D, e: E, f: F, g: G) => H):CustomDecoder<H>;
export function ap<A,B,C,D,E,F,G,H,J>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f_:Decoder<F>, g: Decoder<G>, h: Decoder<H>, f:(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => J):CustomDecoder<J>;
export function ap<A,B,C,D,E,F,G,H,J,K>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f_:Decoder<F>, g: Decoder<G>, h: Decoder<H>, j: Decoder<J>, f:(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, j: J) => K):CustomDecoder<K>;
export function ap<A,B,C,D,E,F,G,H,J,K,L>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f_:Decoder<F>, g: Decoder<G>, h: Decoder<H>, j: Decoder<J>, k: Decoder<K>, f:(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, j: J, k: K) => L):CustomDecoder<L>;
export function ap(...args: Array<Decoder<any> | Function>): Decoder<any> {
return decoder('ap', (val) => {
const func = args[args.length - 1] as Function;
const results: Array<any> = [];
for (let i = 0; i < args.length - 1; i++) {
const dec = args[i] as Decoder<any>;
const ethr = dec.validate(val);
switch(ethr.tag) {
case 'Left': return ethr;
case 'Right': results.push(ethr.value); break;
}
}
return Either.of(func.apply(undefined, results));
});
}
// Примитивы
const anyDecoder = new PrimitiveDecoder<any>('any');
const stringDecoder = new PrimitiveDecoder<string>('string');
const booleanDecoder = new PrimitiveDecoder<boolean>('boolean');
const nullDecoder = new PrimitiveDecoder<null>('null');
const undefinedDecoder = new PrimitiveDecoder<undefined>('undefined');
export const nat = new PrimitiveDecoder<number>('nat');
export const int = new PrimitiveDecoder<number>('int');
export const float = new PrimitiveDecoder<number>('float');
// Экспорт с переименованием
export { anyDecoder as any, stringDecoder as string, booleanDecoder as boolean, nullDecoder as null, undefinedDecoder as undefined };
/**
* Сопоставление с несколькими декодерами до первого успешного
* сравнвния
*/
export function oneOf<A>(a: Decoder<A>): OneOfDecoder<A>;
export function oneOf<A,B>(a: Decoder<A>, b: Decoder<B>): OneOfDecoder<A|B>;
export function oneOf<A,B,C>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>): OneOfDecoder<A|B|C>;
export function oneOf<A,B,C,D>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>): OneOfDecoder<A|B|C|D>;
export function oneOf<A,B,C,D,E>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>): OneOfDecoder<A|B|C|D|E>;
export function oneOf<A,B,C,D,E,F>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>): OneOfDecoder<A|B|C|D|E|F>;
export function oneOf<array extends Decoder<any>[]>(array: array): Decoder<array[number]['_A']>;
export function oneOf(): Decoder<any> {
const decoders = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments);
return new OneOfDecoder(decoders);
}
export function array<A>(decoder: Decoder<A>): Decoder<A[]> {
return new ArrayDecoder(decoder);
}
export function record<fields extends { [k: string]: Decoder<any> }>(fields: fields): RecordDecoder<{[k in keyof fields]: fields[k]['_A'] }> {
return new RecordDecoder(fields);
}
// Вложенные записи в декодерах
export function record2<fields extends { [k: string]: Decoder<any> }>(fields: fields): RecordDecoder<{[k in keyof fields]: fields[k]['_A'] }> {
return new RecordDecoder(fields);
}
// Вложенные записи в декодерах
export function record3<fields extends { [k: string]: Decoder<any> }>(fields: fields): RecordDecoder<{[k in keyof fields]: fields[k]['_A'] }> {
return new RecordDecoder(fields);
}
export const d = array(record({ a: array(record({ b: record({ c: stringDecoder }) })) } ));
export function dict<A>(decoder: Decoder<A>): Decoder<Record<string, A>> {
return new DictionaryDecoder(decoder);
}
export function at<A>(path: string|string[]|number|number[], decoder: Decoder<A>): Decoder<A> {
return new AtDecoder(Array.isArray(path) ? path : [path], decoder);
}
export const date: Decoder<Date> = decoder('date', (value) => {
if (typeof (value) !== 'string') return Either.failure('not a string');
const d = new Date(value);
return isNaN(d.getTime()) ? Either.failure('error parsing date from string') : Either.of(d);
});
/**
* Хелпер для объявления декодеров в стиле Elm. Используется вместе в
* `ap`
*/
export function required<A>(key: string|string[], dec: Decoder<A>): Decoder<A> {
return at(key, dec);
}
/**
* Хелпер для объявления декодеров в стиле Elm. Используется вместе в
* `ap`
*/
export function optional<A>(key: string|string[], dec: Decoder<A>, def: A): Decoder<A>;
export function optional<A,B>(key: string|string[], dec: Decoder<A>, def: B): Decoder<A|B>;
export function optional(key: string|string[], dec: Decoder<any>, def: any): Decoder<any> {
return required(key, dec).withDefault(def);
}
/**
* Создание декодера перечислением всех допустимых значений
*/
export type L = string|number|boolean|null;
export function literals<A extends L>(a: A): Decoder<A>;
export function literals<A extends L, B extends L>(a: A, b: B): Decoder<A|B>;
export function literals<A extends L, B extends L, C extends L>(a: A, b: B, c: C): Decoder<A|B|C>;
export function literals<A extends L, B extends L, C extends L, D extends L>(a: A, b: B, c: C, d: D): Decoder<A|B|C|D>;
export function literals<A extends L, B extends L, C extends L, D extends L, E extends L>(a: A, b: B, c: C, d: D, e: E): Decoder<A|B|C|D|E>;
export function literals<A extends L, B extends L, C extends L, D extends L, E extends L, F extends L>(a: A, b: B, c: C, d: D, e: E, f: F): Decoder<A|B|C|D|E|F>;
export function literals<A extends L, B extends L, C extends L, D extends L, E extends L, F extends L, G extends L>(a: A, b: B, c: C, d: D, e: E, f: F, g: G): Decoder<A|B|C|D|E|F|G>;
export function literals<A extends L, B extends L, C extends L, D extends L, E extends L, F extends L, G extends L, H extends L>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): Decoder<A|B|C|D|E|F|G|H>;
export function literals<A extends L[]>(array: A): Decoder<A[number]>;
export function literals(): Decoder<any> {
const literals: Expr[] = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments);
return new OneOfDecoder(literals.map(x => decoder(v => v === x ? Either.of(v) : Either.failure(`expected ${x}, got ${fancyTypeOf(v)}`))));
}
/**
* Кортежи разных размеров. Проверяемое значение необязательно должно
* быть массивом
* ```ts
* const pair = t.tuple(t.string, t.number);
* const pair_2 = t.record({ '0': t.string, '1': t.number }); // тоже самое
* ```
*/
export function tuple<A>(a: Decoder<A>): Decoder<[A]>;
export function tuple<A, B>(a: Decoder<A>, b: Decoder<B>): Decoder<[A, B]>;
export function tuple<A, B, C>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>): Decoder<[A, B, C]>;
export function tuple<A, B, C, D>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>): Decoder<[A, B, C, D]>;
export function tuple<A, B, C, D, E>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>): Decoder<[A, B, C, D, E]>;
export function tuple<A, B, C, D, E, F>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>): Decoder<[A, B, C, D, E, F]>;
export function tuple<A, B, C, D, E, F, G>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>): Decoder<[A, B, C, D, E, F, G]>;
export function tuple<A, B, C, D, E, F, G, H>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>, h: Decoder<H>): Decoder<[A, B, C, D, E, F, G, H]>;
export function tuple<A, B, C, D, E, F, G, H, I>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>, h: Decoder<H>, i: Decoder<I>): Decoder<[A, B, C, D, E, F, G, H, I]>;
export function tuple<A, B, C, D, E, F, G, H, I, J>(a: Decoder<A>, b: Decoder<B>, c: Decoder<C>, d: Decoder<D>, e: Decoder<E>, f: Decoder<F>, g: Decoder<G>, h: Decoder<H>, i: Decoder<I>, j: Decoder<J>): Decoder<[A, B, C, D, E, F, G, H, I, J]>;
export function tuple<A>(args: Decoder<A>[]): Decoder<A[]>;
export function tuple(): Decoder<any> {
const args: Decoder<any>[] = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.call(arguments);
// @ts-ignore
return ap(...args.map((decoder, idx) => at(idx, decoder)), (...xs) => xs);
}
/**
* Печать проблемы с консоль
*/
export function printProblems(problems: Problem): void {
const lastDecoder = problems.decoders[problems.decoders.length - 1];
console.log(`%cValidation error: ${problems.message}`, 'color: #f00; font-size: 16px; font-weight: 600;')
console.log('%cfailed decoder:', 'font-weight: 600;', lastDecoder.prettyPrint());
console.log('%cfailed value:', 'font-weight: 600;', problems.value);
console.log('%croot value:', 'font-weight: 600;', problems.rootValue);
console.log('%cproblem path:', 'font-weight: 600;', prettyPrintPath(problems.path));
//console.log('decoders ', problems.decoders.map(x => x.prettyPrint()));
}
// Хелпер
function fancyTypeOf(value: any): string {
return Object.prototype.toString.call(value);
}
// Хелпер
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;
}
/**
* Печать декодера.
* ```ts
* const user = t.record({ name: t.string });
* console.log(t.prettyPrint(user)); // => "t.record({ name: t.string })"
* ```
*/
export function prettyPrint(decoder: Decoder<any>): string {
if (decoder instanceof CustomDecoder) return `t.decoder(${JSON.stringify(decoder.name)}, <func>)`;
if (decoder instanceof ArrayDecoder) return `t.array(${decoder.decoder.prettyPrint()})`;
if (decoder instanceof DictionaryDecoder) return `t.dict(${decoder.decoder.prettyPrint()})`;
if (decoder instanceof RecordDecoder) return `t.record({ ${Object.keys(decoder.description).map(k => decoder.description[k].prettyPrint()).join(', ')} })`;
if (decoder instanceof AtDecoder) return `t.at(${JSON.stringify(decoder.path)}, ${decoder.decoder.prettyPrint()})`;
if (decoder instanceof PrimitiveDecoder) return `t.${decoder.primitive}`;
if (decoder instanceof PureDecoder) return `t.of(${JSON.stringify(decoder.value)})`;
if (decoder instanceof ChainDecoder) return `${decoder.prettyPrint()}.chain(<func>)`;
if (decoder instanceof OneOfDecoder) return `t.oneOf(${decoder.alternatives.map(x => x.prettyPrint()).join(', ')})`;
if (decoder instanceof DiscriminateOnDecoder) {
const discriminator = JSON.stringify(decoder.discriminator);
const alternatives = Object.keys(decoder.alternatives).map(k => JSON.stringify(k) + ": " + decoder.alternatives[k].prettyPrint()).join(', ');
return `t.discriminateOn(${}, ${alternatives})`;
}
return absurd(decoder);
}
import * as either from './either';
import { Either } from './either';
import { absurd } from './internal';
// Alias
export type Cmd<Action> = Eff<never, Action>;
// ADT
export type Eff<Error, Success> =
| Pure<Error, Success> // { value: Either<Error, Success> }
| Thunk<Error, Success> // { args: unknown[], run(...args): Either<Error, Success> }
| Subscribe<Error, Success> // { subscribe(onNext: (x: Either<Error, Success>) => void, onComplete: () => void): () => void }
| Batch<Error, Success> // { steps: Eff<Error, Success>[] }
| Concat<Error, Success> // { steps: Eff<Error, Success>[] }
| Apply<Error, Success> // { args: Eff<unknown>[], proj(...args): Either<Error, Success> }
| Chain<Error, Success> // { first: Eff<unknown>, andThen(x: unknown): Eff<Error, Success> }
| HasEffect<Error, Success> // { toEffect(): Eff<Error, Success> }
// Instance methods for `Eff`
export class EffBase<Error, Success> {
readonly _Error: Error;
readonly _Success: Success;
map<Success2>(proj: (value: Success) => Success2): Eff<Error, Success2> {
return this.mapE(ethr => ethr.map(proj));
}
mapTo<Success2>(value: Success2): Eff<Error, Success2> {
return this.mapE(ethr => ethr.mapTo(value));
}
mapError<Error2>(proj: (value: Error) => Error2): Eff<Error2, Success> {
return this.mapE(ethr => ethr.mapLeft(proj));
}
mapErrorTo<Error2>(value: Error2): Eff<Error2, Success> {
return this.mapE(ethr => ethr.mapLeftTo(value));
}
mapE<Error2, Success2>(proj: (x: Either<Error, Success>) => Either<Error2, Success2>): Eff<Error2, Success2> {
// @ts-ignore
return new Apply([this], proj);
}
chain<Success2>(andThen: (x: Success) => Eff<Error, Success2>): Eff<Error, Success2>;
chain<Error2, Success2>(andThen: (x: Success) => Eff<Error2, Success2>): Eff<Error|Error2, Success2>;
chain<Error2, Success2>(andThen: (x: Success) => Eff<Error2, Success2>): Eff<Error|Error2, Success2> {
return new Chain(this.toEff(), (ethr: Either<Error, Success>) => {
return ethr.fold<Eff<Error|Error2, Success2>>(failure, andThen);
});
}
chainTo<Success2>(value: Eff<Error, Success2>): Eff<Error, Success2>;
chainTo<Error2, Success2>(value: Eff<Error2, Success2>): Eff<Error|Error2, Success2>;
chainTo<Error2, Success2>(value: Eff<Error2, Success2>): Eff<Error|Error2, Success2> {
return this.chain(() => value);
}
chainE<Success2>(andThen: (x: Either<Error, Success>) => Eff<Error, Success2>): Eff<Error, Success2>;
chainE<Error2, Success2>(andThen: (x: Either<Error, Success>) => Eff<Error2, Success2>): Eff<Error|Error2, Success2>;
chainE<Error2, Success2>(andThen: (x: Either<Error, Success>) => Eff<Error2, Success2>): Eff<Error|Error2, Success2> {
// @ts-ignore
return new Chain(this.toEff(), andThen);
}
toEff() {
return this as any as Eff<Error, Success>;
}
}
export function of<A>(value: A): Eff<never, A> {
return new Pure(either.success(value));
}
export function success<A>(value: A): Eff<never, A> {
return new Pure(either.success(value));
}
export function failure<A>(value: A): Eff<A, never> {
return new Pure(either.failure(value));
}
export function fromCallback<L, R>(run: (onNext: (x: Either<L, R>) => void, onComplete: () => void) => () => void): Eff<L, R> {
return new Subscribe(run);
}
export function fromPromise<L, R, Args extends unknown[]>(func: (...args: Args) => Promise<Either<L, R>>, ...args: Args): Eff<L, R> {
return new Subscribe((onNext, onComplete) => {
func(...args).then(x => (onNext(x), onComplete()));
return noopFunc;
});
}
export function fromPromise_<L, R>(promise: Promise<R>): Eff<unknown, R> {
return new Subscribe((onNext, onComplete) => {
promise.then(x => (onNext(either.success(x)), onComplete()), e => (onNext(either.failure(e)), onComplete()));
return noopFunc;
});
}
export function fromEither<L, R>(value: Either<L, R>): Eff<L, R> {
return new Pure(value);
}
export function thunk<L, A, Args extends unknown[]>(run: (...args: Args) => Either<L,A>, ...args: Args): Eff<L, A> {
return new Thunk(run as any, args);
}
export { thunk as lazy }; // DEPRECATED
export function ap<L,A,B>(a: Eff<L,A>, f: (a: A) => B): Eff<L,B>;
export function ap<L,A,B,C>(a: Eff<L,A>, b: Eff<L,B>, f: (a: A, b: B) => C): Eff<L,C>;
export function ap<L,A,B,C,D>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, f: (a: A, b: B, c: C) => D): Eff<L,D>;
export function ap<L,A,B,C,D,E>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, f: (a: A, b: B, c: C, d: D) => E): Eff<L,E>;
export function ap<L,A,B,C,D,E,F>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, e: Eff<L,E>, f: (a: A, b: B, c: C, d: D, e: E) => F): Eff<L,F>;
export function ap<L,A,B,C,D,E,F,G>(a: Eff<L,A>, b: Eff<L,B>, c: Eff<L,C>, d: Eff<L,D>, e: Eff<L,E>, f_: Eff<L,F>, f: (a: A, b: B, c: C, d: D, e: E, f: F) => G): Eff<L,G>;
export function ap(): Eff<unknown, unknown> {
const args = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
const proj = arguments[arguments.length - 1];
return new Apply(args, proj);
}
export function record<R extends Record<string, Eff<any, any>>>(rec: R): Eff<{ [K in keyof R]: R[K]['_Error'] }[keyof R], { [K in keyof R]: R[K]['_Success'] }> {
const keys = Object.keys(rec);
return ap.apply(undefined, [...keys.map(k => rec[k]), (...values) => values.reduce((acc, v, idx) => (acc[keys[idx]] = v, acc), {})]);
}
/** Traverse an array */
export function traverse<ERR, A, B>(array: A[], f: (a: A, idx: number) => Eff<ERR, B>): Eff<ERR, B[]> {
if (array.length === 0) return success([]);
return ap.apply(undefined, [...array.map(f), (...args) => args]);
}
/**
* Объединение нескольких параллельно выполняемых `Cmd`
*/
export function batch<Steps extends Cmd<unknown>[]>(...steps: Steps): Cmd<Steps[number]['_Success']>;
export function batch<Steps extends Cmd<unknown>[]>(steps: Steps): Cmd<Steps[number]['_Success']>;
export function batch(): Cmd<unknown> {
const steps = Array.isArray(arguments[0]) ? arguments[0] : arguments;
return new Batch(steps);
}
/**
* Объединение нескольких `Cmd` в очередь
*/
export function concat<Steps extends Cmd<unknown>[]>(...steps: Steps): Cmd<Steps[number]['_Success']>;
export function concat<Steps extends Cmd<unknown>[]>(steps: Steps): Cmd<Steps[number]['_Success']>;
export function concat(): Cmd<unknown> {
const steps = Array.isArray(arguments[0]) ? arguments[0] : arguments;
return new Batch(steps);
}
/**
* Выполнение сайд-еффектов. Observable не генерирует событий, после
* запуска вызывается переданная функция и Observable завершается.
*/
export function forget<Args extends unknown[]>(run: (...args: Args) => unknown, ...args: Args): Cmd<never> {
return new Subscribe((onNext, onComplete) => {
run(...args);
onComplete();
return noopFunc;
});
}
// Functional helpers
const noopFunc = () => {};
// Perform side effects
export function go<Error, Success>(effect: Eff<Error, Success>, onNext: (x: Either<Error, Success>) => void, onComplete: () => void): () => void {
// @ts-ignore
const _this = this;
if (effect instanceof Pure) {
onNext(effect.value);
onComplete();
return noopFunc;
}
if (effect instanceof Thunk) {
onNext(effect.run.apply(_this, effect.args));
onComplete();
return noopFunc;
}
if (effect instanceof Subscribe) {
return effect.subscribe(onNext, onComplete);
}
if (effect instanceof Batch) {
if (effect.steps.length === 0) { onComplete(); return noopFunc; }
let subscriptions: Array<Function|null>;
const loop = idx => () => {
subscriptions[idx] = null;
for (const unsub of subscriptions) if (unsub !== null) return;
onComplete(); // If control flow reaches here, that means all nested commands are completed
};
subscriptions = effect.steps.map((eff, idx) => go(eff, onNext, loop(idx)));
return () => subscriptions.forEach(
funOrNull => funOrNull ? funOrNull() : void 0
);
}
if (effect instanceof Concat) {
let unsubscribe: Function|null = null;
const loop = idx => () => {
// If condition holds, then all nested effects are completed, therefore we're done
if (idx >= effect.steps.length) { onComplete(); return; }
unsubscribe = go(effect.steps[idx], onNext, loop(idx + 1));
};
loop(0);
return () => unsubscribe ? unsubscribe() : void 0;
}
if (effect instanceof Chain) {
const subscriptions: Array<Function|null> = [];
subscriptions.push(go(effect.first, result => {
const idx = subscriptions.length;
subscriptions.push(go(effect.andThen(result), onNext, () => {
subscriptions[idx] = null;
for (const unsub of subscriptions) if (unsub !== null) return;
onComplete();
}));
}, () => {
subscriptions[0] = null;
for (const unsub of subscriptions) if (unsub !== null) return;
onComplete();
}));
return () => subscriptions.forEach(
funOrNull => funOrNull ? funOrNull() : void 0
);
}
if (effect instanceof Apply) {
let allInitialized = false;
let subscriptions: Array<Function|null>;
const initializedFlags: Array<true|undefined> = new Array(effect.args.length);
const recentValues: unknown[] = new Array(effect.args.length);
const next = idx => result => {
recentValues[idx] = result;
check_initialized: {
if (allInitialized) break check_initialized;
initializedFlags[idx] = true;
for (const flag of initializedFlags) if (flag !== true) return;
allInitialized = true;
}
onNext(effect.proj.apply(_this, recentValues));
};
const complete = idx => () => {
subscriptions[idx] = null;
for (const unsub of subscriptions) if (unsub !== null) return;
onComplete();
};
subscriptions = effect.args.map((eff, idx) => go(eff, next(idx), complete(idx)));
return () => subscriptions.forEach(
funOrNull => funOrNull ? funOrNull() : void 0
);
}
if (effect instanceof HasEffect) {
return go(effect.toEffect(), onNext, onComplete);
}
return absurd(effect);
}
export class Pure<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly value: Either<Error, Success>,
) { super(); }
}
export class Thunk<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly run: (...args: unknown[]) => Either<Error, Success>,
readonly args: unknown[],
) { super(); }
}
export class Subscribe<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly subscribe: (onNext: (x: Either<Error, Success>) => void, onComplete: () => void) => () => void,
) { super(); }
}
export class Batch<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly steps: Eff<Error, Success>[],
) { super(); }
}
export class Concat<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly steps: Eff<Error, Success>[],
) { super(); }
}
export class Apply<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly args: Eff<Error, unknown>[],
readonly proj: (...args) => Either<Error, Success>
) { super(); }
}
export class Chain<Error, Success> extends EffBase<Error, Success> {
constructor(
readonly first: Eff<Error, unknown>,
readonly andThen: (x: Either<Error, unknown>) => Eff<Error, Success>
) { super(); }
}
export abstract class HasEffect<Error, Success> extends EffBase<Error, Success> {
abstract toEffect(): Eff<Error, Success>;
}
/**
* Примитивный `Cmd` не генерирует никаких действий, завершается сразу
* после запуска.
*/
export const noop: Cmd<never> = new Batch([]);
export default {
of,
success,
failure,
fromCallback,
fromPromise,
fromPromise_,
fromEither,
thunk,
lazy: thunk,
ap,
record,
batch,
concat,
forget,
go,
noop,
};
/// monadic do-notaion https://github.com/pelotom/burrido
// import Monad from 'burrido'
// export const Do: <err,a>(iter: () => IterableIterator<Eff<err,a>>) => Eff<err,a> = Monad({ pure: of, bind: chain }).Do;
import { Expr } from '~/types';
/** convenient instance methods for `Either` */
export class EitherBase<L,R> {
readonly _L: L;
readonly _R: R;
map<R2>(f: (val: R) => R2): Either<L,R2> {
const self = this as any as Either<L,R>;
switch(self.tag) {
case 'Left': return this as any;
case 'Right': return new Right(f(self.value));
}
}
mapTo<R2>(value: R2): Either<L,R2> {
const self = this as any as Either<L,R>;
switch(self.tag) {
case 'Left': return this as any;
case 'Right': return new Right(value);
}
}
mapLeft<L2>(f: (val: L) => L2): Either<L2,R> {
const self = this as any as Either<L,R>;
switch(self.tag) {
case 'Left': return new Left(f(self.value));
case 'Right': return this as any;
}
}
mapLeftTo<L2>(value: L2): Either<L2,R> {
const self = this as any as Either<L,R>;
switch(self.tag) {
case 'Left': return new Left(value);
case 'Right': return this as any;
}
}
bimap<L2,R2>(f1: (val: L) => L2, f2: (x: R) => R2): Either<L2,R2> {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return new Left(f1(self.value));
case 'Right': return new Right(f2(self.value));
}
}
chain<R2>(f: (val: R) => Either<L,R2>): Either<L,R2>;
chain<L2,R2>(f: (val: R) => Either<L2,R2>): Either<L|L2,R2>;
chain<R2>(f: (val: R) => Either<L,R2>): Either<L,R2> {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return self as any;
case 'Right': return f(self.value);
}
}
chainTo<R2>(value: Either<L,R2>): Either<L,R2>;
chainTo<L2,R2>(value: Either<L2,R2>): Either<L|L2,R2>;
chainTo<R2>(value: Either<L,R2>): Either<L,R2> {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return self as any;
case 'Right': return value;
}
}
fold<T>(f1: (x: L) => T, f2: (x: R) => T): T;
fold<T1,T2>(f1: (x: L) => T1, f2: (x: R) => T2): T1|T2;
fold<T1,T2>(f1: (x: L) => T1, f2: (x: R) => T2): T1|T2 {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return f1(self.value);
case 'Right': return f2(self.value);
}
}
onError(onFailure: (value: L) => Either<L,R>): Either<L,R> {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return onFailure(self.value);
case 'Right': return self;
}
}
onErrorTo(value: Either<L,R>): Either<L,R> {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return value;
case 'Right': return self;
}
}
toNullable(): R|null {
const self = this as any as Either<L, R>;
switch(self.tag) {
case 'Left': return null;
case 'Right': return self.value;
}
}
isRight(): this is Right<R,L> { return this['tag'] === 'Right'; }
isLeft(): this is Left<L,R> { return this['tag'] === 'Left'; }
}
/** adt */
export type Either<L,R> =
| Left<L, R>
| Right<L, R>
;
/** left case */
export class Left<L,R> extends EitherBase<L,R> {
readonly tag: 'Left' = 'Left';
constructor(
readonly value: L,
) { super(); }
}
/** right case */
export class Right<L,R> extends EitherBase<L,R> {
readonly tag: 'Right' = 'Right';
constructor(
readonly value: R,
) { super(); }
}
/** aliases */
export function failure<L extends Expr>(value: L): Either<L, never> { return new Left(value); }
export function success<R extends Expr>(value: R): Either<never, R> { return new Right(value); }
export function left<L extends Expr>(value: L): Either<L, never> { return new Left(value); }
export function right<R extends Expr>(value: R): Either<never, R> { return new Right(value); }
/** alias for `right` */
export function of<A extends Expr>(value: A): Either<never, A> {
return new Right(value);
}
/** apply pure function with multiple arguments */
export function ap<L,A,B>(a: Either<L,A>, f: (a: A) => B): Either<L,B>;
export function ap<L,A,B,C>(a: Either<L,A>, b: Either<L,B>, f: (a: A, b: B) => C): Either<L,C>;
export function ap<L,A,B,C,D>(a: Either<L,A>, b: Either<L,B>, c: Either<L,C>, f: (a: A, b: B, c: C) => D): Either<L,D>;
export function ap<L,A,B,C,D,E>(a: Either<L,A>, b: Either<L,B>, c: Either<L,C>, d: Either<L,D>, f: (a: A, b: B, c: C, d: D) => E): Either<L,E>;
export function ap<L,A,B,C,D,E,F>(a: Either<L,A>, b: Either<L,B>, c: Either<L,C>, d: Either<L,D>, e: Either<L,E>, f: (a: A, b: B, c: C, d: D, e: E) => F): Either<L,F>;
export function ap<L,A,B,C,D,E,F,G>(a: Either<L,A>, b: Either<L,B>, c: Either<L,C>, d: Either<L,D>, e: Either<L,E>, f_: Either<L,F>, f: (a: A, b: B, c: C, d: D, e: E, f: F) => G): Either<L,G>;
export function ap(...args: Array<Either<any, any> | Function>): Either<any, any> {
const func = args[args.length - 1] as Function;
const results: Array<any> = [];
for (let i = 0; i < args.length - 1; i++) {
const ethr = args[i] as Either<any, any>;
switch (ethr.tag) {
case 'Left': return ethr;
case 'Right': results.push(ethr.value); break;
}
}
return new Right(func.apply(undefined, results));
}
/** traverse an array */
export function traverse<L,A,B>(arr: Either<L, A>[]): Either<L,A[]>;
export function traverse<L,A,B>(arr: Array<A>, f: (a: A, idx: number) => Either<L,B>): Either<L,B[]>;
export function traverse<L,A,B>(arr: Array<unknown>, f?: Function): Either<L,unknown[]> {
const output = [] as B[];
for (let i = 0; i < arr.length; i++) {
const ethr = f ? f(arr[i], i) : arr[i];
switch (ethr.tag) {
case 'Left': return ethr as any as Either<L,B[]>;
case 'Right': output.push(ethr.value); break;
}
}
return new Right(output);
}
export const Either = {
Left, Right, failure, success, left, of, ap, traverse,
};
import { HasEffect, Subscribe, Eff } from './eff';
import * as eff from './eff';
import { Decoder, Problem } from './decode';
import { Either } from './either';
import * as either from './either';
/** http method */
export type Method = 'GET'|'POST'|'PUT'|'DELETE'|'PATCH';
/** request */
export interface Request {
url: string;
method: Method;
body?: any;
headers?: Record<string, string|number|undefined|null>;
withCredentials?: boolean;
timeout?: number;
}
export interface RequestProgress extends Request {
progress: true;
}
/** raw error */
export type HttpError =
| { tag: 'BadUrl', desc: string }
| { tag: 'BadPayload', desc: string }
| { tag: 'ValidationProblem', problem: Problem, url: string }
| { tag: 'BadStatus', status: number, desc: string }
| { tag: 'Timeout' }
| { tag: 'NetworkError' }
/** responce */
export interface Response {
url: string;
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
}
/** query params */
export type ParamsPrimitive = number|string|undefined|null;
export type Params = Record<string, ParamsPrimitive|ParamsPrimitive[]>;
/** progress */
export type Progress =
| { tag: 'Computable', total: number, loaded: number }
| { tag: 'Uncomputable' }
export class HttpEffect<A> extends HasEffect<HttpError, A> {
constructor(
readonly request: Request|RequestProgress,
) { super(); };
toEffect() {
return new Subscribe<HttpError, any>((onNext, onComplete) => {
const req = this.request;
const onSuccess = (x: Response) => ('progress' in req && req.progress ? onNext(either.right(either.right(x))) : onNext(either.right(x)), onComplete());
const onProgress = (x: Progress) => 'progress' in req && req.progress ? onNext(either.right(either.left(x))) : void 0;
const onFailure = (x: HttpError) => (onNext(either.left(x)), onComplete());
const xhr = new XMLHttpRequest();
xhr.addEventListener('error', () => onFailure({ tag: 'NetworkError' }));
xhr.addEventListener('timeout', () => onFailure({ tag: 'Timeout' }));
xhr.addEventListener('load', () => onSuccess({
url: xhr.responseURL,
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders()),
body: xhr.response || xhr.responseText,
}));
try {
xhr.open(req.method, req.url, true);
} catch (e) {
onFailure({ tag: 'BadUrl', desc: req.url });
}
if ('progress' in req && req.progress) {
xhr.addEventListener('progress', e => onProgress(e.lengthComputable ? { tag: 'Computable', loaded: e.loaded, total: e.total } : { tag: 'Uncomputable' }));
}
if (req.timeout) xhr.timeout = req.timeout;
if (typeof (req.withCredentials) !== 'undefined') xhr.withCredentials = req.withCredentials;
if (typeof (req.headers) !== 'undefined') {
for (let key in req.headers) {
if (!req.headers.hasOwnProperty(key)) continue;
const value = req.headers[key];
if (typeof(value) !== 'undefined' && value !== null)
xhr.setRequestHeader(key, String(value));
}
}
const body = Object.prototype.toString.apply(req.body) === '[object Object]' ? JSON.stringify(req.body) : req.body;
xhr.send(body);
return () => xhr.abort();
});
}
}
/** send a request */
export function send(req: RequestProgress): HttpEffect<Either<Progress, Response>>;
export function send(req: Request): HttpEffect<Response>;
export function send(req: Request|RequestProgress): HttpEffect<unknown> {
return new HttpEffect(req);
}
/** shortcut for GET requests */
export function get(url: string, request?: Omit<RequestProgress, 'url'|'method'>): HttpEffect<Either<Progress, Response>>;
export function get(url: string, request?: Omit<Request, 'url'|'method'>): HttpEffect<Response>;
export function get(url: string, request?: Omit<Request|RequestProgress, 'url'|'method'>): HttpEffect<unknown> {
return send({ ...request, method: 'GET', url });
}
/** shortcut for POST requests */
export function post(url: string, request?: Omit<RequestProgress, 'url'|'method'>): HttpEffect<Either<Progress, Response>>;
export function post(url: string, request?: Omit<Request, 'url'|'method'>): HttpEffect<Response>;
export function post(url: string, request?: Omit<Request|RequestProgress, 'url'|'method'>): HttpEffect<unknown> {
return send({ ...request, method: 'POST', url });
}
/** parse response as JSON */
export function expectJSON<A>(decoder: Decoder<A>): (resp: Response) => Eff<HttpError, A> {
return resp => {
if (resp.body === '') return eff.failure<HttpError>({ tag: 'BadPayload', desc: 'empty body' });
let val = null;
try {
val = JSON.parse(resp.body);
} catch (e) {
return eff.failure<HttpError>({ tag: 'BadPayload', desc: 'invalid json' });
}
return eff.fromEither(
decoder.validate(val).mapLeft<HttpError>(problem => ({ tag: 'ValidationProblem', problem, url: resp.url }))
);
};
}
/** parse headers from string to dict */
function parseHeaders(rawHeaders: string): Record<string, string> {
const output = {};
const lines = rawHeaders.split('\r\n');
for (let i in lines) {
const index = lines[i].indexOf(': ');
if (index < 0) continue;
const key = lines[i].substring(0, index);
const value = lines[i].substring(index + 2);
output[key] = value;
}
return output;
}
/** build an url */
export function join(...args: Array<string|Params>): string {
let path = '';
let params = {} as Record<string, string>;
let query = '';
for (let i in args) {
const arg = args[i];
if (typeof (arg) === 'string') path = joinTwo(path, arg);
else Object['assign'](params, arg);
}
for (let key in params) {
if (!params.hasOwnProperty(key) || typeof(params[key]) === 'undefined' || params[key] === null) continue;
if (Array.isArray(params[key])) {
for (const v of params[key]) {
if (typeof(params[key]) === 'undefined' || params[key] === null) continue;
query += (query ? '&' : '') + `${encodeURIComponent(key)}=${encodeURIComponent(v)}`;
}
} else {
query += (query ? '&' : '') + `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`;
}
}
return query ? (path + '?' + query) : path;
/** Join segments of url */
function joinTwo(a: string, b: string): string {
if (a === '') return b;
if (b === '') return a;
const trailing = a.length && a[a.length - 1] === '/';
const leading = b.length && b[0] === '/';
if (trailing && leading) return a.substring(0, a.length - 1) + b;
if (!trailing && !leading) return a + '/' + b;
return a + b;
}
}
// Helper
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
import { Expr } from './internal/expr';
/** adt */
export type Option<A> = None<A> | Some<A>
export type Maybe<A> = Option<A>
/** instance method for convenience */
export class OptionBase<A> {
readonly _A: A;
/** map */
map<B>(f: (a: A) => B): Option<B> {
const self = this as any as Option<A>;
switch (self.tag) {
case 'None': return none;
case 'Some': return new Some(f(self.value));
}
}
/** map */
mapTo<B>(value: B): Option<B> {
const self = this as any as Option<A>;
switch (self.tag) {
case 'None': return none;
case 'Some': return some(value);
}
}
/** chain */
chain<B>(f: (a: A) => Option<B>): Option<B> {
const self = this as any as Option<A>;
switch (self.tag) {
case 'None': return none;
case 'Some': return f(self.value);
}
}
/** chain */
chainTo<B>(value: Option<B>): Option<B> {
const self = this as any as Option<A>;
switch (self.tag) {
case 'None': return none;
case 'Some': return value;
}
}
/** unwrap */
unwrap<B extends Expr, C extends Expr>(fromNone: B, fromSome: (x: A) => C): B|C {
const self = this as any as Option<A>;
switch (self.tag) {
case 'None': return fromNone;
case 'Some': return fromSome(self.value);
}
}
/** withDefault */
withDefault<B extends Expr>(def: B): A|B {
return this.unwrap(def, x => x);
}
}
/** empty container */
export class None<A> extends OptionBase<A> {
readonly _a: A;
readonly tag: 'None' = 'None';
}
/** container with a value */
export class Some<A> extends OptionBase<A> {
readonly _a: A;
readonly tag: 'Some' = 'Some';
constructor(
readonly value: A,
) { super(); }
}
/** traverse an array */
export function traverse<A,B>(arr: Array<A>, f: (a: A, idx: number) => Option<B>): Option<B[]> {
const output = [] as B[];
for (let i = 0; i < arr.length; i++) {
const option = f(arr[i], i);
switch (option.tag) {
case 'None': return option as any;
case 'Some': output.push(option.value); break;
}
}
return new Some(output);
}
/** aliases */
export const none = new None<never>();
export function some<A extends Expr>(a: A): Option<A> { return new Some<A>(a); }
export { some as of };
/** For better type inference */
export type Expr = boolean|null|undefined|number|string|{}|any[]|[any,any]|[any,any,any]|Function
/** Don't coerce string literals to `string` type */
export function literal<A extends string>(x: A): A {
return x;
}
/** Helper for totality checking */
export function absurd(x: never): any {
throw new Error('absurd: unreachable code');
}
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