UNPKG

@baqhub/sdk

Version:

The official JavaScript SDK for the BAQ federated app platform.

377 lines (376 loc) 12.3 kB
import { chain, isLeft, isRight } from "fp-ts/lib/Either.js"; import { pipe } from "fp-ts/lib/function.js"; import * as t from "io-ts"; import reporterBase from "io-ts-reporters"; import camelCase from "lodash/camelCase.js"; import isArray from "lodash/isArray.js"; import isNumber from "lodash/isNumber.js"; import isString from "lodash/isString.js"; import map from "lodash/map.js"; import snakeCase from "lodash/snakeCase.js"; import { fixImport } from "./fixImport.js"; import { isDefined } from "./type.js"; export * from "io-ts"; const reporter = fixImport(reporterBase); export const isoDate = new t.Type("DateFromISOString", (u) => u instanceof Date, (u, c) => pipe(t.string.validate(u, c), chain(s => { const d = new Date(s); return isNaN(d.getTime()) ? t.failure(u, c) : t.success(d); })), a => a.toISOString()); // // Readonly + Exact + ChangeCase combinator. // function getProps(codec) { switch (codec._tag) { case "RefinementType": case "ReadonlyType": return getProps(codec.type); case "InterfaceType": case "StrictType": case "PartialType": return codec.props; case "IntersectionType": return codec.types.reduce((props, type) => Object.assign(props, getProps(type)), {}); } } function getExactWithCaseTypeName(codec) { return `ExactWithCase<${codec.name}>`; } function prefixAwareCase(transform) { return (value) => { if (value[0] === "$") { return "$" + transform(value.substring(1)); } return transform(value); }; } function stripKeysAndChangeCase(o, props, transformCheckKey, transformResultKey) { const keys = Object.getOwnPropertyNames(o); let shouldStrip = false; const r = {}; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const checkKey = transformCheckKey(key); const resultKey = transformResultKey(key); const isPropKey = Object.prototype.hasOwnProperty.call(props, checkKey); if (!isPropKey || key !== resultKey) { shouldStrip = true; } if (isPropKey) { r[resultKey] = o[key]; } } return shouldStrip ? r : o; } function exactWithCase(codec, name = getExactWithCaseTypeName(codec)) { const props = getProps(codec); return new t.ExactType(name, codec.is, (u, c) => { const unknownResult = t.UnknownRecord.validate(u, c); if (isLeft(unknownResult)) { return unknownResult; } const strippedObject = stripKeysAndChangeCase(unknownResult.right, props, prefixAwareCase(camelCase), prefixAwareCase(camelCase)); return codec.validate(strippedObject, c); }, a => { const encoded = codec.encode(a); return stripKeysAndChangeCase(encoded, props, t.identity, prefixAwareCase(snakeCase)); }, codec); } export function object(props, name) { return t.readonly(exactWithCase(t.type(props, name))); } export function partialObject(props, name) { return t.readonly(exactWithCase(t.partial(props, name))); } export function dualObject(props, partialProps) { return t.intersection([object(props), partialObject(partialProps)]); } // // Standard union. // function pushAll(xs, ys) { const l = ys.length; for (let i = 0; i < l; i++) { xs.push(ys[i]); } } export function union(codecs) { const name = `Union(${codecs.map(type => type.name).join(" | ")})`; return new t.UnionType(name, (u) => codecs.some(type => type.is(u)), (u, c) => { const errors = []; let result; for (let i = 0; i < codecs.length; i++) { const codec = codecs[i]; const r = codec.validate(u, t.appendContext(c, String(i), codec, u)); if (isLeft(r)) { pushAll(errors, r.left); } else if (t.UnknownRecord.is(u)) { result = result ? { ...result, ...r.right, } : r.right; } else { return t.success(r.right); } } if (isDefined(result)) { return t.success(result); } return t.failures(errors); }, codecs.every(c => c.encode === t.identity) ? t.identity : a => { let result; for (const codec of codecs) { if (!codec.is(a)) { continue; } const value = codec.encode(a); if (t.UnknownRecord.is(value)) { const value = codec.encode(a); result = result ? { ...result, ...value, } : value; } else { return value; } } if (isDefined(result)) { return result; } // https://github.com/gcanti/io-ts/pull/305 throw new Error(`no codec found to encode value in union type ${name}`); }, codecs); } // // Todo: Optimized union for records. // // // Exclusive union. // export function exclusiveUnion(codecs) { const name = `ExclusiveUnion(${codecs.map(type => type.name).join(" | ")})`; return new t.UnionType(name, (u) => codecs.some(type => type.is(u)), (u, c) => { const errors = []; const successes = []; for (let i = 0; i < codecs.length; i++) { const codec = codecs[i]; const r = codec.validate(u, t.appendContext(c, String(i), codec, u)); if (isLeft(r)) { errors.push(...r.left); } else { successes.push(r.right); } } if (successes.length === 1) { return t.success(successes[0]); } else if (successes.length > 1) { return t.failure(u, c, "Multiple matching codecs."); } else { return t.failures(errors); } }, codecs.every(c => c.encode === t.identity) ? t.identity : a => { for (const codec of codecs) { if (codec.is(a)) { return codec.encode(a); } } // https://github.com/gcanti/io-ts/pull/305 throw new Error(`no codec found to encode value in union type ${name}`); }, codecs); } // // Default value. // export function defaultValue(type, defaultValue) { return new t.Type("DefaultOf" + type.name, type.is, value => { if (!isDefined(value)) { return t.success(defaultValue); } return type.decode(value); }, value => { if (value === defaultValue) { return undefined; } return type.encode(value); }); } // // Array that ignores invalid elements. // export function arrayIgnore(itemsType) { return new t.Type("ArrayIgnoreOf" + itemsType.name, (value) => { if (!isArray(value)) { return false; } return value.every(value => itemsType.is(value)); }, (value, context) => { if (!isArray(value)) { return t.failure(value, context); } const array = value .map(item => itemsType.decode(item)) .filter(isRight) .map(item => item.right); return t.success(array); }, value => { return value.map(item => itemsType.encode(item)); }); } // // Partial record combinator for enums. // export function enumRecord(domain, codomain) { return t.record(domain, codomain); } export function listEnumValues(sourceEnum) { function isStringKey(key) { const numberKey = Number(key); return isNaN(numberKey) || sourceEnum[sourceEnum[key] || ""] !== numberKey; } function keyToValue(key) { return sourceEnum[key]; } return Object.keys(sourceEnum).filter(isStringKey).map(keyToValue); } function enumerationBase(name, sourceEnum) { const enumValues = new Set(listEnumValues(sourceEnum)); function isEnumValue(value) { return (isString(value) || isNumber(value)) && enumValues.has(value); } return new t.Type(name, isEnumValue, (value, context) => { if (isEnumValue(value)) { return t.success(value); } return t.failure(value, context); }, value => value); } export function enumeration(sourceEnum) { return enumerationBase("Enum", sourceEnum); } export function weakEnumeration(sourceEnum) { return enumerationBase("WeakEnum", sourceEnum); } function toLowerCase(source) { return source.toLowerCase(); } function enumerationWithValuesBase(name, sourceEnum, values, { isCaseSensitive } = { isCaseSensitive: true }) { // Case sensitivity. const valueTransform = isCaseSensitive ? t.identity : toLowerCase; // Mapper. const invertedValues = map(values, (value, key) => [valueTransform(value), key]); const valueMap = new Map(invertedValues); // Type guard. const enumValues = new Set(listEnumValues(sourceEnum)); function isEnumValue(value) { return (isString(value) || isNumber(value)) && enumValues.has(value); } return new t.Type(name, isEnumValue, (value, context) => { const enumValue = isString(value) && valueMap.get(valueTransform(value)); if (!enumValue) { return t.failure(value, context); } return t.success(enumValue); }, value => values[value]); } export function enumerationWithValues(sourceEnum, values, options) { return enumerationWithValuesBase("EnumWithValues", sourceEnum, values, options); } export function weakEnumerationWithValues(sourceEnum, values, options) { return enumerationWithValuesBase("WeakEnumWithValues", sourceEnum, values, options); } // // Bytes type. // class Base64Bytes extends t.Type { constructor() { function is(value) { return value instanceof Uint8Array; } function validate(value, context) { if (is(value)) { return t.success(value); } try { const valueBytes = atob(String(value)); return t.success(Uint8Array.from(valueBytes, c => c.charCodeAt(0))); } catch (_error) { return t.failure(value, context); } } function encode(value) { const bytes = String.fromCharCode.apply(null, value); return btoa(bytes); } super("Base64Bytes", is, validate, encode); } } export const base64Bytes = new Base64Bytes(); class Utf8Bytes extends t.Type { constructor() { const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); function is(value) { return value instanceof Uint8Array; } function validate(value, context) { if (is(value)) { return t.success(value); } try { return t.success(textEncoder.encode(String(value))); } catch (_error) { return t.failure(value, context); } } function encode(value) { return textDecoder.decode(value); } super("Utf8Bytes", is, validate, encode); } } export const utf8Bytes = new Utf8Bytes(); export function optional(model) { return union([t.undefined, model]); } export function clean(model) { return model; } export function validate(model, value) { if (!model.is(value)) { throw new Error("Error while validating."); } return value; } export function tryDecode(model, value) { const result = model.decode(value); if (isLeft(result)) { return undefined; } return result.right; } export function decode(model, value) { const result = model.decode(value); if (isLeft(result)) { console.log(...reporter.report(result)); throw new Error("Error while decoding."); } return result.right; } export function encode(model, value) { return model.encode(value); }