Commit 681c0ec6 by Vladislav Lagunov

Добавлена старая версия @bitmaster/utils

parent 3ce695a4
/**
* Функции для работы с тэгами локали в формате BCP47
* @see http://tools.ietf.org/html/bcp47
*/
/**
* Принимает локаль в формате BCP47 или в формате POSIX (со знаком
* подчеркивания в качестве разделителя), нормализует в BCP47
* @param {string} locale - строка локали в POSIX или BCP47
* @return {string} локаль в формате BCP47
*/
export function normalizeLocale(locale) {
return locale.replace(/(.+)_/g, '$1-');
}
/**
* @param {string} locale - локаль в формате BCP47 для поиска
* @param {array} availableLocales - массив доступных локалей в формате BCP47
* @return {string|undefined} locale если эта локаль есть в списке
* availableLocales, иначе наиболее подходящаяя локаль из этого списка
*/
export function lookupLocale(locale, availableLocales) {
if (availableLocales.findIndex((l) => (l === locale)) !== -1) {
return locale; // locale присутствует в списке доступных локалей
}
const chunks = locale.split('-');
const similarLocale = availableLocales.find((l) => (l.startsWith(chunks[0])));
if (similarLocale !== undefined) return similarLocale;
return undefined;
}
import { Decoder } from '../decoder';
import * as t from '../decoder';
import { Either } from '../either';
import * as either from '../either';
/** a source to read config from */
export type ConfigSource =
| { tag: 'Cli', prefix: string, args: string[] }
| { tag: 'Location/search', value: string, transformKey(x: string): string }
| { tag: 'Config', value: Record<string, any> }
/** a decoder together with a function that reads a string into an arbitrary value (string is usually from get-parameters) */
export class ConfigReader<A> {
readonly _A: A;
constructor(
readonly decoder: Decoder<A>,
readonly fromString?: (x: string) => any,
) {}
/** provide default value */
withDefault<B>(a: B): ConfigReader<A|B> {
return new ConfigReader(this.decoder.withDefault(a), this.fromString);
}
/** apply pure function */
map<B>(f: (x: A) => B): ConfigReader<B> { return new ConfigReader(this.decoder.map(f), this.fromString); }
}
/** type alias */
export type ConfigRecord = Record<string, ConfigReader<any>>;
/** a source to read config from */
type ConfigSourceProcessed =
| { tag: 'Cli', value: Record<string, string> }
| { tag: 'Location/search', value: Record<string, string> }
| { tag: 'Config', value: Record<string, any> }
/** fold configs */
function foldConfigs<R extends ConfigRecord>(sources: ConfigSource[], config: R): Either<{ output: Partial<{ [K in keyof R]: R[K]['_A']}>, problem: t.Problem }, { [K in keyof R]: R[K]['_A'] }> {
const processedSources = sources.map(function (source: ConfigSource): ConfigSourceProcessed {
switch (source.tag) {
case '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('=');
const cfg = config[k];
if (config.hasOwnProperty(k) && cfg.fromString) value[k] = cfg.fromString(v);
}
return { tag: 'Cli', value };
}
case '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) => (acc[source.transformKey(decodeURIComponent(k))] = v ? decodeURIComponent(v) : '', acc), {} as any);
return { tag: 'Location/search', value };
}
case 'Config': return source;
}
});
/** fold all sources into one record */
const record: Record<string, any> = {};
for (let key in config) {
for (let i in processedSources) {
const source = processedSources[i];
if (source.tag === 'Cli' && source.value.hasOwnProperty(key)) {
record[key] = source.value[key];
break;
}
if (source.tag === 'Location/search' && source.value.hasOwnProperty(key)) {
const cfg = config[key];
cfg.fromString && (record[key] = cfg.fromString(source.value[key]));
break;
}
if (source.tag === 'Config' && source.value.hasOwnProperty(key)) {
record[key] = source.value[key];
break;
}
}
}
const output = {} as { [K in keyof R]: R[K]['_A']};
for (let key in config) {
if (!config.hasOwnProperty(key)) continue;
const ethr = config[key].decoder.validate(record[key]);
switch(ethr.tag) {
case 'Left': { ethr.value.path.push(key); return either.failure({ output, problem: ethr.value }); }
case 'Right': output[key] = ethr.value; break;
}
}
return either.success(output);
}
/** read config */
export function read<R extends Record<string, ConfigReader<any>>>(sources: ConfigSource[], config: R): Either<t.Problem, { [K in keyof R]: R[K]['_A']}> {
const ethr = foldConfigs(sources, config);
return ethr.tag === 'Right' ? ethr as any : either.failure(ethr.value.problem);
}
/** primitives */
export const booleanReader = new ConfigReader(t.boolean, x => { if (x === 'false') return false; if (x === '0') return false; return true; });
export const floatReader = new ConfigReader(t.float, parseFloat);
export const intReader = new ConfigReader(t.int, parseInt);
export const natReader = new ConfigReader(t.nat, parseInt);
export const stringReader = new ConfigReader(t.string, x => x);
export const anyReader = new ConfigReader(t.any, x => x);
export function array<T>(cr: ConfigReader<T>): ConfigReader<T[]> {
return new ConfigReader(t.array(cr.decoder));
}
/** constructor */
export function reader<a>(fromString: (x: string) => any, decoder: Decoder<a>): ConfigReader<a> {
return new ConfigReader(decoder, fromString);
}
/** export primitives */
export { booleanReader as boolean, floatReader as float, intReader as int, natReader as nat, stringReader as string, anyReader as any };
import * as jed from 'jed';
import { memoize } from 'lodash';
/** translations */
export interface Translations {
locale_data: { messages: Record<string, any> };
domain: string;
debug?: boolean;
}
/** application context with translations */
export interface Ctx {
translations: Translations|null;
}
/** For convenience also allow passing `Ctx` instead of just translations */
export type CtxOrMaybeTranslations = Ctx|Translations|null;
/** lazy string for deferred translation */
export type I18nString = (t: CtxOrMaybeTranslations) => string;
export type Gettext = (a: string) => string;
/** minimal sptrintf */
export function sprintf(format: string, ...args: any[]): string {
var i = 0;
return format.replace(/%(s|d)/g, function() {
return args[i++];
});
}
export function gettext(t: CtxOrMaybeTranslations, key: string): string {
return getJedIntance(resolveTranslations(t)).gettext(key);
}
export function pgettext(t: CtxOrMaybeTranslations, context: string, key: string): string {
return getJedIntance(resolveTranslations(t)).pgettext(context, key);
}
export function dcgettext(t: CtxOrMaybeTranslations, domain: string, key: string, context: string): string {
return getJedIntance(resolveTranslations(t)).dcgettext(domain, key, context);
}
export function dcnpgettext(t: CtxOrMaybeTranslations, domain: string, context: string, single: string, plural: string, n: number): string {
return getJedIntance(resolveTranslations(t)).dcnpgettext(domain, context, single, plural, n);
}
/** defered versions of functions */
export namespace deferred {
export function gettext(key: string): I18nString {
return t => getJedIntance(resolveTranslations(t)).gettext(key);
}
export function pgettext(context: string, key: string): I18nString {
return t => getJedIntance(resolveTranslations(t)).pgettext(context, key);
}
export function dcgettext(domain: string, key: string, context: string): I18nString {
return t => getJedIntance(resolveTranslations(t)).dcgettext(domain, key, context);
}
export function dcnpgettext(domain: string, context: string, single: I18nString, plural: I18nString, n: number): I18nString {
return t => getJedIntance(resolveTranslations(t)).dcnpgettext(domain, context, single(t), plural(t), n);
}
export function sprintf(format: I18nString, ...args: any[]): I18nString {
return translations => {
const formatString = typeof(format) === 'string' ? format : format(translations);
var i = 0;
return formatString.replace(/%(s|d)/g, function() { return args[i++]; });
}
}
}
function resolveTranslations(t: CtxOrMaybeTranslations): Translations {
return !t ? defaultTranslations : t.hasOwnProperty('translations') ? (t['translations'] || defaultTranslations) : t as Translations;
}
/** memoized `Jed` constructor */
const getJedIntance = memoize((translations: Translations) => new jed.Jed(translations));
/** minimal empty translations jed needs */
export const defaultTranslations: Translations = { "locale_data" : { "messages" : { "" : { "domain": "messages", "lang": "en", "plural_forms" : "nplurals=2; plural=(n != 1);" } } }, "domain" : "messages", "debug" : false }
import * as t from '@bitmaster/core/decode';
import { Decoder, decoder, required, optional, Validation } from '@bitmaster/core/decode';
import { success, failure } from '@bitmaster/core/either';
/** tolarate numbers as strings */
export const stringOrNumber: Decoder<string> = decoder(
'stringOrNumber', v => typeof (v) === 'string' || typeof (v) === 'number' ? success(String(v)) : failure('not a string nor a number')
);
/** top level http://jsonapi.org/format/#document-top-level */
export interface Document {
data: Resource | Resource[] | null | undefined;
meta: Record<string, JSON> | null;
included: Resource[] | null;
jsonapi: Jsonapi | null;
links: { [key: string]: Link } | null;
}
/** a document, containing errors */
export interface Errors {
errors: ErrorObject[],
meta: Object | null;
included: Resource[] | null;
jsonapi: Jsonapi | null;
links: { [key: string]: Link } | null;
}
/** error obeject http://jsonapi.org/format/#error-objects */
export interface ErrorObject {
id: string | null;
links: { [s: string]: Link } | null;
status: string | null;
code: string | null;
title: string | null;
detail: string | null;
source: { pointer: string, parameter: string } | null;
}
/** jsonapi resource http://jsonapi.org/format/#document-resource-objects */
export interface Resource {
id: string;
type: string;
attributes: { [k: string]: any } | null;
relationships: { [k: string]: Relationship } | null;
meta: Object | null;
}
/** link object http://jsonapi.org/format/#document-links */
export type Link = string | {
href: string;
meta: Object | null;
}
/** attributes http://jsonapi.org/format/#document-resource-object-attributes */
export type Attributes = Object;
/** relationship http://jsonapi.org/format/#document-resource-object-relationships */
export interface Relationship {
data: Linkage;
meta: Object | null;
links: { [k: string]: Link } | null;
}
/** linkage http://jsonapi.org/format/#document-resource-object-linkage */
export type Linkage = null | Identifier | Identifier[]
/** resource identifier http://jsonapi.org/format/#document-resource-identifier-objects */
export interface Identifier {
id: string;
type: string;
meta: object|null;
}
/** jsonapi version http://jsonapi.org/format/#document-jsonapi-object */
export interface Jsonapi {
version: string;
}
/** identifier validation */
export const identifier = t.ap(
required('id', stringOrNumber),
required('type', t.string),
optional('meta', tolerantDict(t.any), null),
(id, type, meta) => ({ id, type, meta })
);
/** linkage validation */
export const linkage = t.oneOf(t.null, identifier, t.array(identifier));
/** link validation */
export const link = t.oneOf(t.string, t.ap(
required('href', t.string),
optional('meta', tolerantDict(t.any), null),
(href, meta) => ({ href, meta })
));
/** relationship validation */
export const relationship = t.ap(
required('data', linkage),
optional('meta', tolerantDict(t.any), null),
optional('links', tolerantDict(link), null),
(data, meta, links) => ({ data, meta, links })
);
/** resource validation */
export const resource: Decoder<Resource> = t.ap(
required('id', stringOrNumber),
required('type', t.string),
optional('attributes', tolerantDict(t.any), null),
optional('relationships', tolerantDict(relationship), null),
optional('meta', tolerantDict(t.any), null),
(id, type, attributes, relationships, meta) => ({ id, type, attributes, relationships, meta })
);
/** jsonapi version validation */
export const jsonapi = required('version', t.string).map(version => ({ version }));
/** document validation */
export const document = t.ap(
optional('data', t.oneOf(resource, t.array(resource), t.null), undefined),
optional('meta', tolerantDict(t.any), null),
optional('included', t.array(resource), null),
optional('jsonapi', jsonapi, null),
optional('links', tolerantDict(link), null),
(data, meta, included, jsonapi, links) => ({ data, meta, included, jsonapi, links })
);
/** decode error object */
export const errorObject = t.ap(
optional('id', t.string, null),
optional('links', tolerantDict(link), null),
optional('status', stringOrNumber, null), /* TODO: actualy can only be a string */
optional('code', t.string, null),
optional('title', t.string, null),
optional('detail', t.string, null),
optional('details', t.string, null), /* HACK: due to TM quirks, this field doesn't appear in specification */
optional('source', t.record({ pointer: t.string, parameter: t.string }), null),
(id, links, status, code, title, detail, details, source) =>
({ id, links, status, code, title, detail: detail || details, source })
);
/** errors decoder */
export const errors = t.ap(
required('errors', t.array(errorObject)),
optional('meta', tolerantDict(t.any), null),
optional('jsonapi', jsonapi, null),
optional('links', tolerantDict(link), null),
(errors, meta, jsonapi, links) => ({ errors, meta, jsonapi, links })
);
/** validate attributes */
export function attributes<A>(res: Resource, dec: Decoder<A>): Validation<A> {
return dec.validate(res.attributes);
}
/** validate primary resource */
export function primary(doc: Document): Validation<Resource> {
return Object.prototype.toString.call(doc.data) === '[object Object]'
? success(doc.data as Resource)
: failure(`jsonapi.primary: jsonapi document doesn't contain primary resorce`);
}
/** validate resource collection */
export function collection(doc: Document): Validation<Resource[]> {
return Array.isArray(doc.data)
? success(doc.data)
: failure('jsonapi.collection: jsonapi document doesn\'t contain collection of resorces');
}
/** meta */
export function meta<A>(doc: Document, dec: Decoder<A>): Validation<A> {
return dec.validate(doc.meta);
}
/** validate related resource */
export function related(doc: Document, res: Resource, name: string): Validation<Resource> {
if (doc.included && res.relationships && res.relationships[name]) {
const rel: Relationship = res.relationships[name];
if (rel.data !== null && !Array.isArray(rel.data)) {
const { id, type } = rel.data;
const result = doc.included.find(a => a.id === id && a.type === type);
return result ? success(result) : failure(`jsonapi.related: cannot find related resource ${name} -> '${type}', #${id} for '${res.type}', #${res.id}`) as Validation<Resource>;
}
}
return failure(`jsonapi.related: cannot find related resource ${name} for '${res.type}', #${res.id}`);
}
/** validate related resource (one-to-many) */
export function relatedCollection(doc: Document, res: Resource, name: string): Validation<Resource[]> {
if (doc.included && res.relationships && res.relationships[name]) {
const rel: Relationship = res.relationships[name];
if (Array.isArray(rel.data)) {
const acc: Resource[] = [];
for (let i in rel.data) {
const { id, type } = rel.data[i];
const result = doc.included.find(a => a.id === id && a.type === type);
if (result) {
acc.push(result);
} else {
return failure(`jsonapi.relatedCollection: cannot find related resource '${name}', id: ${id}`);
}
}
return success(acc);
}
}
return failure(`jsonapi.relatedCollection: cannot find related resource '${name}' for '${res.type}', #${res.id}`);
}
/** validate related resource (only identifier) */
export function relatedLinkage(res: Resource, name: string): Validation<Identifier> {
if (res.relationships && res.relationships[name]) {
const rel: Relationship = res.relationships[name];
return rel.data !== null && !Array.isArray(rel.data) ? success(rel.data) : failure(`jsonapi.relatedLinkage: cannot find relationship '${name}' for ${res.type}#${res.id}`);
}
return failure(`relatedLinkage: trying to access relationship '${name}' on ${res.type}#${res.id}: resource doesn't have relationships`);
}
/** validate related resource collection (only identifiers) */
export function relatedLinkageCollection(res: Resource, name: string): Validation<Identifier[]> {
if (res.relationships && res.relationships[name]) {
const rel: Relationship = res.relationships[name];
return Array.isArray(rel.data) ? success(rel.data) : failure(`relatedLinkageCollection: cannot find relationship '${name}' for ${res.type}#${res.id}`);
}
return failure(`relatedLinkageCollection: trying to access relationship '${name}' on ${res.type}#${res.id}: resource doesn't have relationships`);
}
/** unlike `t.dict` this version ignores invalid values */
export function tolerantDict<A>(d: Decoder<A>): Decoder<Record<string, A>> {
return decoder('jsonapi.tolerantDict', value => {
if (Object.prototype.toString.call(value) !== '[object Object]') return failure('not an object');
const output = {} as Record<string, A>;
for (let key in value) {
if (!value.hasOwnProperty(key)) continue;
const ethr = d.validate(value[key]);
switch(ethr.tag) {
case 'Left': continue;
case 'Right': output[key] = ethr.value; break;
}
}
return success(output);
});
}
import * as decoders from './decoders';
export * from './resources';
export { decoders };
/** function to compare two argument lists */
export type ArgsComparator = (xs: IArguments, ys: IArguments) => boolean;
/** compare arguments as references */
export function cmpPtr(xs: IArguments, ys: IArguments): boolean {
if (xs.length !== ys.length) return false;
for (let i in xs) { if (xs[i] !== ys[i]) return false; }
return true;
}
/** cheap partial memoisation, only the last call results are preserved */
export default function memoize<A>(func: () => A, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B>(func: (a: A) => B, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B,C>(func: (a: A, b: B) => C, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B,C,D>(func: (a: A, b: B, c: C) => D, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B,C,D,E>(func: (a: A, b: B, c: C, d: D) => E, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B,C,D,E,F>(func: (a: A, b: B, c: C, d: D, e: E) => F, cmp?: ArgsComparator): typeof func;
export default function memoize<A,B,C,D,E,F,G>(func: (a: A, b: B, c: C, d: D, e: E, f: F) => G, cmp?: ArgsComparator): typeof func;
export default function memoize<A>(func: (...args) => A, cmp: ArgsComparator = cmpPtr): typeof func {
let lastArguments: IArguments|null = null;
let lastResult: any = undefined;
return function(this: any) {
const result = lastArguments && cmp(lastArguments, arguments) ? lastResult : func.apply(this, arguments);
lastResult = result;
lastArguments = arguments;
return result;
};
}
import { Eff, eff, Either, Decoder, success, failure, decode as t } from '../core';
import * as aesjs from 'aes-js';
/** options for `Cache` */
export interface CacheOptions {
key?: number[] | Uint8Array;
version?: string;
lifetime?: number;
expires?: number;
}
/** possible errors */
export type Err =
| { tag: 'NoItem' }
| { tag: 'SizeLimitExeeded' }
| { tag: 'InvalidPayload', problem: t.Problem }
| { tag: 'VersionMismatch', actual: string|null, expected: string }
/** persistent cache */
export class Cache<A> {
readonly _A: A;
constructor(
public name: string, // should be unique
public decoder: Decoder<A>,
public options: CacheOptions = {},
) {}
saveSync(payload: A): Either<Err,null> {
try {
const updatedAt = Date.now();
const createdAt = Date.now();
const version = this.options.version || null;
let stringContent = JSON.stringify({ updatedAt, createdAt, version, payload });
if (this.options.key) {
const enc = new aesjs.ModeOfOperation.ctr(this.options.key);
const bytes = aesjs.utils.utf8.toBytes(stringContent);
stringContent = aesjs.utils.hex.fromBytes(enc.encrypt(bytes));
}
localStorage.setItem(this.name, stringContent);
} catch(e) {
return failure({ tag: 'SizeLimitExeeded' } as Err);
}
return success(null);
}
save(payload: A): Eff<Err,null> {
return eff.lazy(() => this.saveSync(payload));
}
readSync(): Either<Err,A> {
let stringContent = localStorage.getItem(this.name);
if (this.options.key && stringContent !== null) {
const enc = new aesjs.ModeOfOperation.ctr(this.options.key);
const bytes = aesjs.utils.hex.toBytes(stringContent)
stringContent = aesjs.utils.utf8.fromBytes(enc.decrypt(bytes));
}
if (stringContent === null) return failure({ tag: 'NoItem' } as Err);
try {
const json = JSON.parse(stringContent);
return cacheItemDecoder(this.decoder).validate(json)
.mapLeft(problem => ({ tag: 'InvalidPayload', problem } as Err))
.chain(({payload, version}) => {
if (this.options.version && version !== version) {
return failure<Err>({ tag: 'VersionMismatch', expected: this.options.version, actual: version })
}
return success(payload);
});
} catch (e) {
return failure({ tag: 'InvalidPayload', problem: e.message } as Err);
}
}
read(): Eff<Err,A> {
return eff.lazy(() => this.readSync());
}
clearSync(): void {
localStorage.removeItem(this.name);
}
clear(): Eff<never, null> {
return eff.lazy(() => (this.clearSync(), success(null)));
}
modifySync(f: (a: A) => A): Either<Err, null> {
const orignal = this.readSync();
if (orignal.tag === 'Left') return orignal as any;
return this.saveSync(f(orignal.value));
}
modify(f: (a: A) => A): Eff<Err, null> {
return eff.lazy(() => this.modifySync(f));
}
}
// Data stored in storage
export interface CacheItem<A> {
createdAt: number;
updatedAt: number;
version: string|null;
payload: A;
}
// Decode a `CacheItem`
const cacheItemDecoder = <A>(decoder: Decoder<A>) => t.record({
createdAt: t.float,
updatedAt: t.float,
version: t.oneOf(t.string, t.null),
payload: decoder,
});
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