@baqhub/sdk
Version:
The official JavaScript SDK for the BAQ federated app platform.
377 lines (376 loc) • 12.3 kB
JavaScript
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);
}