Commit 8783a0ae by Vladislav Lagunov

Добавление нового функционала

parent 679120a6
import * as React from 'react'; import * as React from 'react';
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { FieldProps } from '~/fields/context'; import { FieldProps } from '~/fields/context';
import { filterMap } from '~/utils'; import filterMap from '~/functions/filter-map';
import * as F from '~/fields'; import { Modifier } from '~/fields';
// Props // Props
...@@ -32,6 +32,6 @@ export default class ArrayIDS<A = any> extends React.Component<Props<A>> { ...@@ -32,6 +32,6 @@ export default class ArrayIDS<A = any> extends React.Component<Props<A>> {
}); });
render() { render() {
return <F.Modifier proj={this.projectContext}>{this.props.children}</F.Modifier> return <Modifier proj={this.projectContext}>{this.props.children}</Modifier>
} }
} }
...@@ -6,13 +6,13 @@ import { FieldProps } from '~/fields'; ...@@ -6,13 +6,13 @@ import { FieldProps } from '~/fields';
import TextField from '~/fields/TextField'; import TextField from '~/fields/TextField';
import { Props as TextFieldProps } from '~/fields/TextField'; import { Props as TextFieldProps } from '~/fields/TextField';
import { StandardProps } from '@material-ui/core'; import { StandardProps } from '@material-ui/core';
import Icon from '~/components/Icon'; import Icon from './Icon';
import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles'; import withStyles, { StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme'; import { Theme } from '@material-ui/core/styles/createMuiTheme';
import { Source, State as AutoCompleteState } from '~/fields/AutoComplete/state-machine'; import { Source, State as AutoCompleteState } from '~/fields/AutoComplete/state-machine';
import * as ST from '~/fields/AutoComplete/state-machine'; import * as ST from '~/fields/AutoComplete/state-machine';
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
import { memoize } from '~/utils'; import memoize from '~/functions/memoize';
import { pick } from 'lodash'; import { pick } from 'lodash';
import classNames = require('classnames'); import classNames = require('classnames');
......
import { Err } from "~/context"; import { Err, AuthCtx } from "~/context";
import { Cmd, noop, Eff, cmd } from "@bitmaster/core"; import { Cmd, noop, Eff, cmd } from "@bitmaster/core";
import { PendingAction, pending, AuthCtx } from "~/utils";
/**
* Экшен для управления флагом загрузки
*/
export type PendingAction =
| { tag: '@Pending', pending: boolean }
// Pending
export function pending<A>(command: Cmd<A>): Cmd<A> {
return cmd.concat(
cmd.of({ tag: '@Pending', pending: true } as never),
command,
cmd.of({ tag: '@Pending', pending: false } as never),
);
}
// Контекст // Контекст
...@@ -81,7 +97,7 @@ export function init<A>(): State<A> { ...@@ -81,7 +97,7 @@ export function init<A>(): State<A> {
export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [State<A>, Cmd<Action<A>>] { export function update<A>(ctx: Ctx<A>, action: Action<A>, state: State<A>): [State<A>, Cmd<Action<A>>] {
const onFailure = (error: Err) => ({ tag: 'Error', error } as Action<A>); const onFailure = (error: Err) => ({ tag: 'Error', error } as Action<A>);
switch(action.tag) { switch(action.tag) {
case 'Pending': { case '@Pending': {
return [{ ...state, pending: action.pending }, noop]; return [{ ...state, pending: action.pending }, noop];
} }
case 'Error': { case 'Error': {
......
import * as React from 'react';
import Icon, { IconProps } from '@material-ui/core/Icon';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import * as classNames from 'classnames';
// Component
class IconComponent extends React.Component<IconProps & WithStyles> {
render() {
const { classes, className } = this.props;
const rootClass = classNames(className, {
[classes.button]: 'onClick' in this.props
});
// @ts-ignore
return React.createElement(Icon, { ...this.props, className: rootClass });
}
}
export default withStyles(styles)(IconComponent);
// Styles
export function styles(theme: Theme): StyleRules {
// const { unit } = theme.spacing;
return {
button: {
cursor: 'pointer',
color: theme.palette.text.secondary,
'&:hover': {
color: theme.palette.text.primary,
}
},
};
}
...@@ -6,7 +6,7 @@ import { FieldProps } from './'; ...@@ -6,7 +6,7 @@ import { FieldProps } from './';
import * as moment from 'moment'; import * as moment from 'moment';
import TextField, { Props as TextFieldProps } from './TextField'; import TextField, { Props as TextFieldProps } from './TextField';
import { StandardProps, Popover } from '@material-ui/core'; import { StandardProps, Popover } from '@material-ui/core';
import Icon from '~/components/Icon'; import Icon from './Icon';
import 'react-dates/initialize'; import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css'; import 'react-dates/lib/css/_datepicker.css';
import DayPicker from 'react-dates/lib/components/DayPicker'; import DayPicker from 'react-dates/lib/components/DayPicker';
......
import * as React from 'react'; import * as React from 'react';
import { CrudResource } from '~/jsonapi';
import { AuthCtx as Ctx } from '~/utils'; import { AuthCtx as Ctx } from '~/utils';
import { Eff } from '@bitmaster/core'; import { Eff } from '@bitmaster/core';
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
...@@ -11,7 +10,6 @@ import { Resource } from '@bitmaster/utils/jsonapi'; ...@@ -11,7 +10,6 @@ import { Resource } from '@bitmaster/utils/jsonapi';
// Props // Props
export type Props<A = any> = { export type Props<A = any> = {
children: React.ReactElement<{ resource: CrudResource<A, never> }>;
source: PrimarySource<A>; source: PrimarySource<A>;
id?: string; id?: string;
} }
......
...@@ -11,7 +11,7 @@ import PendingOverlay from '@bitmaster/components/PendingOverlay'; ...@@ -11,7 +11,7 @@ import PendingOverlay from '@bitmaster/components/PendingOverlay';
import { LocaleCtx, Gettext, withGettext } from '~/gettext'; import { LocaleCtx, Gettext, withGettext } from '~/gettext';
// props // Props
export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string> & WithStyles<string> & { export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string> & WithStyles<string> & {
__: Gettext; __: Gettext;
ctx: LocaleCtx; ctx: LocaleCtx;
...@@ -29,13 +29,13 @@ export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string ...@@ -29,13 +29,13 @@ export type Props<A=any> = StandardProps<React.HTMLProps<HTMLDivElement>, string
} }
// state // State
export interface State { export interface State {
selected: number; selected: number;
} }
// component // Component
class Suggestions<A> extends React.Component<Props<A>, State> { class Suggestions<A> extends React.Component<Props<A>, State> {
state = { selected: 0 }; state = { selected: 0 };
paperEl: HTMLElement|null = null; paperEl: HTMLElement|null = null;
...@@ -217,7 +217,7 @@ export function styles(theme): StyleRules { ...@@ -217,7 +217,7 @@ export function styles(theme): StyleRules {
empty: { empty: {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
padding: [unit, unit * 2], padding: [unit, unit * 2] as any,
textAlign: 'center', textAlign: 'center',
fontStyle: 'italic', fontStyle: 'italic',
width: '100%', width: '100%',
......
...@@ -2,7 +2,8 @@ import * as React from 'react'; ...@@ -2,7 +2,8 @@ import * as React from 'react';
import { FieldProps } from './'; import { FieldProps } from './';
import * as F from './'; import * as F from './';
import { StandardProps } from '@material-ui/core'; import { StandardProps } from '@material-ui/core';
import { memoize, ObjectKey, ObjectPath } from '~/utils'; import memoize from '~/functions/memoize';
import { ObjectKey, ObjectPath } from '~/functions/get-at';
// Props // Props
...@@ -31,7 +32,6 @@ const zoomAny = memoize((path: ObjectKey[], input: any) => { ...@@ -31,7 +32,6 @@ const zoomAny = memoize((path: ObjectKey[], input: any) => {
// Component // Component
export default class Zoom extends React.Component<Props> { export default class Zoom extends React.Component<Props> {
projectContext = (input: FieldProps) => { projectContext = (input: FieldProps) => {
const { onlyValue } = this.props; const { onlyValue } = this.props;
const path = Array.isArray(this.props.in) ? this.props.in : [this.props.in]; const path = Array.isArray(this.props.in) ? this.props.in : [this.props.in];
......
import * as React from 'react'; import * as React from 'react';
import { AuthCtx as Ctx } from '~/context'; import { AuthCtx as Ctx } from '~/context';
import { I18nString } from '~/gettext'; import { I18nString } from '~/gettext';
import { ObjectPath } from '~/utils'; import { ObjectPath } from '~/functions/get-at';
const hoistNonReactStatics = require('hoist-non-react-statics'); const hoistNonReactStatics = require('hoist-non-react-statics');
......
/**
* Комбинация `Array.prototype.filter` и `Array.prototype.map`
*/
export default function filterMap<A, B>(xs: A[], f: (a: A) => B|null): B[] {
return xs.reduce<B[]>((acc, a) => { const b = f(a); b !== null && acc.push(b); return acc; }, []);
}
/**
* Ссылка на поле со вложенных объектах
*/
export type ObjectKey = string|number;
export type ObjectPath = ObjectKey|ObjectKey[];
/**
* Получение значения вложенного поля
* ```ts
* const a = { one: { two: { three: 42 } };
* const b = getAt(['one', 'two', 'three'])(a);
* // b => 42
* ```
*/
export default function getAt<R=any>(at_: ObjectPath): <A>(a: A) => R {
const at = Array.isArray(at_) ? at_ : [at_];
return a => {
let iter = a as any as R;
for (const k of at) {
if (!iter.hasOwnProperty(k)) return undefined as any as R; // Поле отсутствует
iter = iter[k];
}
return iter;
}
}
import _memoize from 'memoize-one';
import { isEqual } from 'lodash';
// Функция для сравнения аргументов
export type IsEqual = (a: ArrayLike<any>, b: ArrayLike<any>) => boolean;
// Мемоизация
export default function memoize<A>(func: () => A, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B>(func: (a: A) => B, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B,C>(func: (a: A, b: B) => C, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B,C,D>(func: (a: A, b: B, c: C) => D, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B,C,D,E>(func: (a: A, b: B, c: C, d: D) => E, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B,C,D,E,F>(func: (a: A, b: B, c: C, d: D, e: E) => F, isEqual_?: IsEqual): 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, isEqual_?: IsEqual): typeof func;
export default function memoize<A,B,C,D,E,F,G,H>(func: (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => H, isEqual_?: IsEqual): typeof func;
export default function memoize<A>(func: (...args) => A, isEqual_?: IsEqual): typeof func {
return _memoize(func, isEqual_ || isEqual);
}
import { ObjectPath } from './get-at';
/**
* Обновляет значение вложенного поля
* ```ts
* const a = { one: { two: { three: 42 } } };
* const b = updateAt(['one', 'two', 'three'], 'four')(a);
* // b => { one: { two: { three: 'four' } } }
* ```
*/
export function updateAt(at_: ObjectPath, value: any): <A>(obj: A) => A {
const at = Array.isArray(at_) ? at_ : [at_];
return obj => {
let stack: any[] = [obj];
let tail: any = stack[0];
for (const k of at) {
if (!tail.hasOwnProperty(k)) return obj; // Поле отсутствует
stack.push(tail[k]);
tail = tail[k];
}
return stack.reduceRight((acc, x, idx) => {
if (idx === stack.length - 1) return value;
if (Array.isArray(x)) {
const output = x.slice();
output.splice(at[idx] as number, 1, acc);
return output;
}
return ({ ...x, [at[idx]]: acc });
}, value);
};
}
import * as React from 'react';
import { Omit } from '@material-ui/core';
import memoize from '~/functions/memoize';
// Делимитер контекста
const CONTEXT_DELIMITER = String.fromCharCode(4);
// Translations
export type Translations = Record<string, Record<string, string[]|string>>;
export type GettextData = {
'': Record<string, unknown>;
'%explicit': GettextExplicitKey[],
} & {
[K in string]: string|string[];
};
// Содержимое поля `%explicit`
export type GettextExplicitKey = {
msgid: string;
msgid_plural?: string;
msgctxt?: string;
msgstr: string|string[];
}
// Хелпер для перевода
export type Gettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => string;
export type DeferredGettext = (singular_key: string, context?: string, plural_key?: string, val?: number) => I18nString;
export type I18nString = (locale?: string|LocaleCtx|null) => string;
// Использование локали из `ctx`
export type LocaleCtx = { locale: string };
/**
* Создание хелпера для переводов
*/
export function makeGettext(...webpackContexts): DeferredGettext {
const translations: Translations = assignData({}, ...flattenModules(webpackContexts));
return (singular_key: string, context?: string, plural_key: string = singular_key, n: number = 1) => locale_ => {
let locale = !locale_ ? 'en-US' : typeof(locale_) === 'string' ? locale_ : locale_.locale;
locale = locale.replace(/_/, '-');
if (!(locale in translations)) {
// Не найдена локаль в `translations`
return n === 1 ? singular_key : plural_key;
}
const key = context ? context + CONTEXT_DELIMITER + singular_key : singular_key;
const dict = translations[locale];
const pluralIdx = getPluralForm(locale)(n);
const pluralList = dict[key] || [singular_key, plural_key];
return nth(pluralList, pluralIdx);
};
function flattenModules(xs: any[]) {
return Array.prototype.concat.apply([], xs.map(x => {
return Array.isArray(x) ? flattenModules(x) : [x]
}));
}
function nth<A>(xs: A|A[], idx: number): A {
if (!Array.isArray(xs)) return xs;
return idx >= xs.length ? xs[xs.length - 1] : xs[idx];
}
};
/**
* HOC для добавления переводов (для разделения переводов по фичам)
* ```ts
* export default withGettext(require('./i18n'))();
* ```
*/
export function withGettext(...webpackContexts) {
const gettext = typeof(webpackContexts[0]) === 'function' ? webpackContexts[0] : makeGettext(...webpackContexts);
return <P extends { __: Gettext, ctx?: { locale: string } }>(Component: React.ComponentType<P>) => {
class WithTranslations extends React.Component<Omit<P, '__'>> {
makeGettext: (locale?: string|LocaleCtx) => Gettext = memoize((locale) => (...args) => gettext.apply(void 0, args)(locale))
render() {
const { ctx } = this.props;
// @ts-ignore
const locale = ctx ? (ctx.locale || 'en-US') : 'en-US';
return React.createElement(Component as any, { ...this.props as any, __: this.makeGettext(locale) });
}
}
// @ts-ignore
WithTranslations.name = 'WithTranslations(' + Component.name + ')';
return WithTranslations;
};
}
/**
* Хелпер для импорта нескольких переводов
* ```ts
* export default requireTranslations(require.context('./i18', false, /\.po$/));
* ```
*/
export function requireTranslations(webpackContext /*: WebpackContext*/): Translations {
return webpackContext.keys().reduce((acc, k) => mergeTranslations(acc, webpackContext(k)), {});
function mergeTranslations(acc, data) {
data = data.__esModule ? data.default : data;
for (const k of Object.keys(data)) {
acc[k] = acc[k] || {};
Object.assign(acc[k], data[k]);
}
return acc;
}
}
/**
* Добавление переводов из `srcs` в `dst`
*/
function assignData(dst: Translations, ...srcs: GettextData[]): Translations {
srcs.forEach(data => {
const locale = data[''].locale; if (!locale || typeof(locale) !== 'string') return;
const explicitKeys = data['%explicit'];
for (const k of Object.keys(data)) {
if (k === '' || k === '%pairs') continue;
dst[locale] = dst[locale] || {};
Object.assign(dst[locale], data[k]);
}
if (explicitKeys) for (const explicit of explicitKeys) {
const key = explicit.msgctxt ? explicit.msgctxt + CONTEXT_DELIMITER + explicit.msgid : explicit.msgid;
dst[locale] = dst[locale] || {};
dst[locale][key] = explicit.msgstr;
}
});
return dst;
}
/**
* Минималистичный `sprintf`. Только простая замена %s на позиционные аргументы
*/
export function sprintf(format: string, ...args: any[]): string {
var i = 0;
return format.replace(/%(s|d)/g, function() {
return args[i++];
});
}
export namespace deferred {
/**
* Минималистичный `sprintf`. Только простая замена %s на позиционные аргументы
*/
export function sprintf(format: I18nString, ...args: any[]): I18nString {
return locale => {
var i = 0;
return format(locale).replace(/%(s|d)/g, function() {
return args[i++];
});
}
}
}
/**
* Вычисление множественной формы для указанного языка. Если язык не
* распознан применяются правила для
* https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#Plural_rule_1_(2_forms)
* http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
*/
export function getPluralForm(locale: string): (n: number) => number {
const [lang] = locale.split(/[_\-]/);
// Славянские языки, 3 формы
if (lang === 'ru' || lang === 'be' || lang === 'bs' || lang === 'hr' || lang === 'sh' || lang === 'sr_ME' || lang === 'uk') {
return n => (n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2);
}
// Литовский
if (lang === 'lt') {
return n => (n % 10 == 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2);
}
// Арабский 6 форм
if (lang === 'ar' || lang === 'ars') {
return n => (n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5))));
}
// Бретонский 5 форм
if (lang === 'br') {
return n => (n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91) ? 0 : ((n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92) ? 1 : ((((n % 10 == 3 || n % 10 == 4) || n % 10 == 9) && (n % 100 < 10 || n % 100 > 19) && (n % 100 < 70 || n % 100 > 79) && (n % 100 < 90 || n % 100 > 99)) ? 2 : ((n != 0 && n % 1000000 == 0) ? 3 : 4)));
}
// Чешский и словакский, 3 формы
if (lang === 'cz' || lang === 'sk') {
return n => (n == 1) ? 0 : ((n >= 2 && n <= 4) ? 1 : 2);
}
// Уельский, 6 форм
if (lang === 'cy') {
return n => (n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));
}
// Словенский, лужицкие сербы, 4 формы
if (lang === 'sl' || lang === 'dsb' || lang === 'hsb') {
return n => (n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3));
}
// TODO http://mlocati.github.io/cldr-to-gettext-plural-rules/
return n => n == 1 ? 0 : 1;
}
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, unknown> | 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 };
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