tynder
Version:
TypeScript friendly Data validator for JavaScript.
857 lines • 30.8 kB
JavaScript
// Copyright (c) 2019 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln
import { dummyTargetObject, isUnsafeVarNames } from './lib/protection';
// emulate Pick<T> // ex. Pick<Foo, 'a' | 'b'>
export function picked(ty, ...names) {
switch (ty.kind) {
case 'object':
{
const members = [];
for (const name of names) {
const member = ty.members.find(x => x[0] === name);
if (member) {
if (member[2]) {
const m2 = [...member];
if (3 < m2.length) {
m2[2] = false;
}
else {
m2.length = 2;
}
members.push(m2);
}
else {
members.push(member);
}
}
}
return ({
kind: 'object',
members,
});
}
case 'symlink':
case 'operator':
{
return ({
kind: 'operator',
operator: 'picked',
operands: [ty, ...names],
});
}
default:
return ({
kind: 'object',
members: [],
});
}
}
// emulate Omit<T> // ex. Omit<Foo, 'a' | 'b'>
export function omit(ty, ...names) {
switch (ty.kind) {
case 'object':
{
const members = [];
for (const member of ty.members) {
if (!names.find(name => member[0] === name)) {
if (member[2]) {
const m2 = [...member];
if (3 < m2.length) {
m2[2] = false;
}
else {
m2.length = 2;
}
members.push(m2);
}
else {
members.push(member);
}
}
}
return ({
kind: 'object',
members,
});
}
case 'symlink':
case 'operator':
{
return ({
kind: 'operator',
operator: 'omit',
operands: [ty, ...names],
});
}
default:
return ({
kind: 'object',
members: [],
});
}
}
// emulate Partial<T>
export function partial(ty) {
switch (ty.kind) {
case 'object':
{
const members = [];
for (const member of ty.members) {
let m = member[1].kind === 'optional' ?
member :
[member[0], optional(member[1]), ...member.slice(2)];
if (m[2]) {
m = [...m];
if (3 < m.length) {
m[2] = false;
}
else {
m.length = 2;
}
}
m[1].name = m[0];
const optTy = Object.assign({}, m[1].optional);
m[1].optional = optTy;
if (optTy.name && optTy.name !== optTy.typeName) {
delete optTy.name;
}
if (!optTy.name && optTy.typeName) {
optTy.name = optTy.typeName;
}
members.push(m);
}
return ({
kind: 'object',
members,
});
}
case 'symlink':
case 'operator':
{
return ({
kind: 'operator',
operator: 'partial',
operands: [ty],
});
}
default:
return ty;
}
}
// intersection (a & b)
export function intersect(...types) {
if (types.length === 0) {
throw new Error(`Empty intersection type is not allowed.`);
}
if (0 < types.filter(x => x && typeof x === 'object' &&
(x.kind === 'symlink' || x.kind === 'operator')).length) {
return ({
kind: 'operator',
operator: 'intersect',
operands: types.slice(),
});
}
let lastTy = null;
const members = new Map();
for (const ty of types) {
if (ty && typeof ty === 'object') {
if (lastTy && lastTy.kind !== ty.kind) {
return ({
kind: 'never',
});
}
lastTy = ty;
if (ty.kind === 'object') {
for (const m of ty.members) {
if (m[2]) {
const m2 = [...m];
if (3 < m2.length) {
m2[2] = false;
}
else {
m2.length = 2;
}
members.set(m[0], m2); // Overwrite if exists
}
else {
members.set(m[0], m); // Overwrite if exists
}
}
}
}
else {
return ({
kind: 'never',
});
}
}
if (lastTy && lastTy.kind !== 'object') {
return lastTy;
}
else {
return ({
kind: 'object',
members: Array.from(members.values()),
});
}
}
// union (a | b)
export function oneOf(...types) {
if (types.length === 0) {
throw new Error(`Empty union type is not allowed.`);
}
if (types.length === 1) {
const ty = types[0];
if (ty && typeof ty === 'object') {
return ty;
}
else {
return primitiveValue(ty);
}
}
const ret = {
kind: 'one-of',
oneOf: [],
};
for (const ty of types) {
// TODO: remove same type
if (ty && typeof ty === 'object') {
if (ty.kind === 'one-of') {
ret.oneOf = ret.oneOf.concat(ty.oneOf);
}
else {
ret.oneOf.push(ty);
}
}
else {
ret.oneOf.push(primitiveValue(ty));
}
}
return ret;
}
// subtraction (a - b)
export function subtract(...types) {
if (types.length === 0) {
throw new Error(`Empty subtraction type is not allowed.`);
}
if (0 < types.filter(x => x && typeof x === 'object' &&
(x.kind === 'symlink' || x.kind === 'operator')).length) {
return ({
kind: 'operator',
operator: 'subtract',
operands: types.slice(),
});
}
let ret = types[0];
if (!ret || typeof ret !== 'object' || ret.kind !== 'object') {
throw new Error(`First parameter of subtraction type should be 'object'.`);
}
for (const ty of types.slice(1)) {
if (ty && typeof ty === 'object' && ty.kind === 'object') {
ret = omit(ret, ...ty.members.map(m => m[0]));
}
}
return ret;
}
export function primitive(typeName) {
switch (typeName) {
case 'never':
return ({
kind: 'never',
});
case 'any':
return ({
kind: 'any',
});
case 'unknown':
return ({
kind: 'unknown',
});
case 'number':
// FALL_THRU
case 'integer':
// FALL_THRU
case 'bigint':
// FALL_THRU
case 'string':
// FALL_THRU
case 'boolean':
// FALL_THRU
case 'null':
// FALL_THRU
case 'undefined':
return ({
kind: 'primitive',
primitiveName: typeName,
});
case 'never?':
return (optional({
kind: 'never',
}));
case 'any?':
return (optional({
kind: 'any',
}));
case 'unknown?':
return (optional({
kind: 'unknown',
}));
case 'number?':
// FALL_THRU
case 'integer?':
// FALL_THRU
case 'bigint?':
// FALL_THRU
case 'string?':
// FALL_THRU
case 'boolean?':
// FALL_THRU
case 'null?':
// FALL_THRU
case 'undefined?':
return (optional({
kind: 'primitive',
primitiveName: typeName.substring(0, typeName.length - 1),
}));
default:
throw new Error(`Unknown primitive type assertion: ${typeName}`);
}
// TODO: Function, DateStr, DateTimeStr, Funtion?, DateStr?, DateTimeStr?
}
export function regexpPatternStringType(pattern) {
return ({
kind: 'primitive',
primitiveName: 'string',
pattern,
});
}
export function primitiveValue(value) {
if (value === null || value === void 0) {
return ({
kind: 'primitive-value',
value,
});
}
else
switch (typeof value) {
case 'number':
// FALL_THRU
case 'bigint':
// FALL_THRU
case 'string':
// FALL_THRU
case 'boolean':
return ({
kind: 'primitive-value',
value,
});
default:
throw new Error(`Unknown primitive value assertion: ${value}`);
}
}
export function optional(ty) {
if (ty && typeof ty === 'object' && ty.kind) {
if (ty.kind === 'optional') {
return ty;
}
else {
return (Object.assign({ kind: 'optional', optional: ty }, (ty.typeName ? { typeName: ty.typeName } : {})));
}
}
else {
return ({
kind: 'optional',
optional: primitiveValue(ty),
});
}
}
export function repeated(ty, option) {
if (ty && typeof ty === 'object' && ty.kind) {
return ({
kind: 'repeated',
min: option && typeof option.min === 'number' ? option.min : null,
max: option && typeof option.max === 'number' ? option.max : null,
repeated: ty,
});
}
else {
return ({
kind: 'repeated',
min: option && typeof option.min === 'number' ? option.min : null,
max: option && typeof option.max === 'number' ? option.max : null,
repeated: primitive(ty),
});
}
}
export function sequenceOf(...seq) {
return ({
kind: 'sequence',
sequence: seq.map(ty => ty && typeof ty === 'object' && ty.kind ? ty : primitiveValue(ty)),
});
}
export function spread(ty, option) {
if (ty && typeof ty === 'object' && ty.kind) {
return ({
kind: 'spread',
min: option && typeof option.min === 'number' ? option.min : null,
max: option && typeof option.max === 'number' ? option.max : null,
spread: ty,
});
}
else {
return ({
kind: 'spread',
min: option && typeof option.min === 'number' ? option.min : null,
max: option && typeof option.max === 'number' ? option.max : null,
spread: primitiveValue(ty),
});
}
}
export function enumType(...values) {
const ar = values.slice();
let value = 0;
for (let i = 0; i < ar.length; i++) {
if (isUnsafeVarNames(dummyTargetObject, ar[i][0])) {
throw new Error(`Unsafe symbol name is appeared in enum assertion: ${ar[i][0]}`);
}
if (ar[i][1] === null || ar[i][1] === void 0) {
ar[i][1] = value++;
}
else if (typeof ar[i][1] === 'number') {
value = ar[i][1] + 1;
}
if (!ar[i][2]) {
ar[i].length = 2;
}
}
return ({
kind: 'enum',
values: ar,
});
}
export function objectType(...members) {
const revMembers = members.slice().reverse();
for (const x of members) {
if (typeof x[0] === 'string') {
if (isUnsafeVarNames(dummyTargetObject, x[0])) {
throw new Error(`Unsafe symbol name is appeared in object assertion: ${x[0]}`);
}
if (members.find(m => m[0] === x[0]) !== revMembers.find(m => m[0] === x[0])) {
throw new Error(`Duplicated member is found: ${x[0]}`);
}
}
}
const membersProps = members
.filter(x => typeof x[0] === 'string')
.map(x => x[1] && typeof x[1] === 'object' && x[1].kind ?
[x[0], withName(x[1], x[0]), x[2]] :
[x[0], withName(primitiveValue(x[1]), x[0]), x[2]])
.map(x => (x[2] ?
[x[0], x[1], false, ...x.slice(2)] :
[x[0], x[1]]));
const additionalProps = members
.filter(x => typeof x[0] !== 'string')
.map(x => x[1] && typeof x[1] === 'object' && x[1].kind ?
x :
[x[0], primitiveValue(x[1]), x[2]])
.map(x => (x[2] ?
[x[0], x[1], false, ...x.slice(2)] :
[x[0], x[1]]));
return (Object.assign({
kind: 'object',
members: membersProps,
}, (0 < additionalProps.length ? {
additionalProps,
} : {})));
}
function checkRecursiveExtends(ty, base) {
if (ty === base) {
return false;
}
if (ty.typeName &&
(ty.typeName === base.typeName ||
(base.kind === 'symlink' && ty.typeName === base.symlinkTargetName))) {
return false;
}
if (base.kind === 'object' && base.baseTypes) {
for (const z of base.baseTypes) {
if (!checkRecursiveExtends(ty, z)) {
return false;
}
}
}
return true;
}
export function derived(ty, ...exts) {
const ret = {
kind: 'object',
members: [],
baseTypes: [],
};
for (const ext of exts) {
switch (ext.kind) {
case 'object':
if (!checkRecursiveExtends(ty, ext)) {
throw new Error(`Recursive extend is found: ${ty.name || '(unnamed)'}`);
}
for (const m of ext.members) {
if (!ret.members.find(x => x[0] === m[0])) {
ret.members.push([m[0], m[1], true, ...m.slice(3)]);
}
// TODO: Check for different types with the same name.
}
// FALL_THRU
case 'symlink':
ret.baseTypes.push(ext);
break;
case 'operator':
{
throw new Error(`Unresolved type operator is found: ${ext.operator}`);
}
}
// NOTE: 'symlink' base types will resolved by calling `resolveSymbols()`.
// `resolveSymbols()` will call `derived()` after resolve symlink exts.
}
ret.members = ty.members.concat(ret.members);
if (ty.baseTypes) {
ret.baseTypes = ty.baseTypes
.filter(x => x.kind !== 'symlink')
.concat(ret.baseTypes);
}
if (ret.baseTypes.length === 0) {
delete ret.baseTypes;
}
const revMembers = ret.members.slice().reverse();
for (const x of ret.members) {
if (ret.members.find(m => m[0] === x[0]) !== revMembers.find(m => m[0] === x[0])) {
throw new Error(`Duplicated member is found: ${x[0]} in ${ty.name || '(unnamed)'}`);
}
}
let additionalProps = [];
if (ret.baseTypes) {
for (const base of ret.baseTypes) {
if (base.kind === 'object') {
if (base.additionalProps && 0 < base.additionalProps.length) {
additionalProps = additionalProps.concat(base.additionalProps.map(x => [x[0], x[1], true, ...x.slice(3)]));
}
}
// NOTE: 'symlink' base types will resolved by calling `resolveSymbols()`.
// `resolveSymbols()` will call `derived()` after resolve symlink exts.
}
}
if (ty.additionalProps && 0 < ty.additionalProps.length) {
additionalProps = additionalProps.concat(ty.additionalProps); // TODO: concat order
}
if (0 < additionalProps.length) {
ret.additionalProps = additionalProps;
}
return ret;
}
export function symlinkType(name) {
return ({
kind: 'symlink',
symlinkTargetName: name,
});
}
export function withName(ty, name) {
if (!name) {
return ty;
}
return (Object.assign(Object.assign({}, ty), { name }));
}
export function withTypeName(ty, typeName) {
if (!typeName) {
return ty;
}
return (Object.assign(Object.assign({}, ty), { typeName }));
}
export function withOriginalTypeName(ty, originalTypeName) {
if (!originalTypeName) {
return ty;
}
return (Object.assign(Object.assign({}, ty), { originalTypeName }));
}
export function withDocComment(ty, docComment) {
if (!docComment) {
return ty;
}
return (Object.assign(Object.assign({}, ty), { docComment }));
}
export function withRange(minValue, maxValue) {
return (ty) => {
if (typeof minValue !== 'number' && typeof minValue !== 'string') {
throw new Error(`Decorator '@range' parameter 'minValue' should be number or string.`);
}
if (typeof maxValue !== 'number' && typeof maxValue !== 'string') {
throw new Error(`Decorator '@range' parameter 'maxValue' should be number or string.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@range' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { minValue, maxValue }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@range' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { minValue, maxValue }));
}
};
}
export function withMinValue(minValue) {
return (ty) => {
if (typeof minValue !== 'number' && typeof minValue !== 'string') {
throw new Error(`Decorator '@minValue' parameter 'minValue' should be number or string.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@minValue' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { minValue }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@minValue' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { minValue }));
}
};
}
export function withMaxValue(maxValue) {
return (ty) => {
if (typeof maxValue !== 'number' && typeof maxValue !== 'string') {
throw new Error(`Decorator '@maxValue' parameter 'maxValue' should be number or string.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@maxValue' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { maxValue }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@maxValue' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { maxValue }));
}
};
}
export function withGreaterThan(greaterThanValue) {
return (ty) => {
if (typeof greaterThanValue !== 'number' && typeof greaterThanValue !== 'string') {
throw new Error(`Decorator '@greaterThan' parameter 'greaterThan' should be number or string.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@greaterThan' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { greaterThanValue }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@greaterThan' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { greaterThanValue }));
}
};
}
export function withLessThan(lessThanValue) {
return (ty) => {
if (typeof lessThanValue !== 'number' && typeof lessThanValue !== 'string') {
throw new Error(`Decorator '@lessThan' parameter 'lessThan' should be number or string.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@lessThan' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { lessThanValue }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@lessThan' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { lessThanValue }));
}
};
}
export function withMinLength(minLength) {
return (ty) => {
if (typeof minLength !== 'number') {
throw new Error(`Decorator '@minLength' parameter 'minLength' should be number.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@minLength' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { minLength }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@minLength' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { minLength }));
}
};
}
export function withMaxLength(maxLength) {
return (ty) => {
if (typeof maxLength !== 'number') {
throw new Error(`Decorator '@maxLength' parameter 'maxLength' should be number.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@maxLength' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { maxLength }) }));
}
else {
if (!ty || ty.kind !== 'primitive') {
throw new Error(`Decorator '@maxLength' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { maxLength }));
}
};
}
export function withMatch(pattern) {
return (ty) => {
if (typeof pattern !== 'object') {
throw new Error(`Decorator '@match' parameter 'pattern' should be RegExp.`);
}
if (ty.kind === 'optional') {
const opt = ty.optional;
if (opt.kind !== 'primitive') {
throw new Error(`Decorator '@match' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { pattern }) }));
}
else {
if (!ty || ty.kind !== 'primitive' || ty.primitiveName !== 'string') {
throw new Error(`Decorator '@match' cannot be applied to anything other than 'primitive'.`);
}
return (Object.assign(Object.assign({}, ty), { pattern }));
}
};
}
export function withStereotype(stereotype) {
if (typeof stereotype !== 'string') {
throw new Error(`Decorator '@stereotype' parameter 'stereotype' should be string.`);
}
if (isUnsafeVarNames(dummyTargetObject, stereotype)) {
throw new Error(`Unsafe symbol name is appeared in stereotype assertion: ${stereotype}`);
}
return (ty) => {
if (ty.kind === 'optional') {
const ret = (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, ty.optional), { stereotype }) }));
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { stereotype }));
return ret;
}
};
}
export function withConstraint(name, args) {
if (typeof name !== 'string') {
throw new Error(`Decorator '@constraint' parameter 'name' should be string.`);
}
if (isUnsafeVarNames(dummyTargetObject, name)) {
throw new Error(`Unsafe symbol name is appeared in constraint assertion: ${name}`);
}
return (ty) => {
if (ty.kind === 'optional') {
const opt = ty.optional;
const ret = (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, opt), { customConstraints: opt.customConstraints
? opt.customConstraints.slice().push(name)
: [name], customConstraintsArgs: opt.customConstraintsArgs
? Object.assign(Object.assign({}, opt.customConstraintsArgs), { [name]: args }) : { [name]: args } }) }));
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { customConstraints: ty.customConstraints
? ty.customConstraints.slice().push(name)
: [name], customConstraintsArgs: ty.customConstraintsArgs
? Object.assign(Object.assign({}, ty.customConstraintsArgs), { [name]: args }) : { [name]: args } }));
return ret;
}
};
}
export function withForceCast() {
return (ty) => {
if (ty.kind === 'optional') {
const ret = (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, ty.optional), { forceCast: true }) }));
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { forceCast: true }));
return ret;
}
};
}
export function withRecordType() {
return (ty) => {
if (ty.kind === 'optional') {
const ret = (Object.assign(Object.assign({}, ty), { optional: Object.assign(Object.assign({}, ty.optional), { isRecordTypeField: true }) }));
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { isRecordTypeField: true }));
return ret;
}
};
}
export function withMeta(meta) {
return (ty) => {
const ret = (Object.assign(Object.assign({}, ty), { meta }));
return ret;
};
}
export function withMsg(messages) {
return (ty) => {
if (ty.kind === 'optional') {
if (typeof messages === 'string') {
const ret = (Object.assign(Object.assign({}, ty), { message: messages, optional: Object.assign(Object.assign({}, ty.optional), { message: messages }) }));
delete ret.messages;
delete ret.optional.messages;
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { messages, optional: Object.assign(Object.assign({}, ty.optional), { messages }) }));
delete ret.message;
delete ret.optional.message;
return ret;
}
}
else {
if (typeof messages === 'string') {
const ret = (Object.assign(Object.assign({}, ty), { message: messages }));
delete ret.messages;
return ret;
}
else {
const ret = (Object.assign(Object.assign({}, ty), { messages }));
delete ret.message;
return ret;
}
}
};
}
export function withMsgId(messageId) {
return (ty) => {
if (ty.kind === 'optional') {
return (Object.assign(Object.assign({}, ty), { messageId, optional: Object.assign(Object.assign({}, ty.optional), { messageId }) }));
}
else {
return (Object.assign(Object.assign({}, ty), { messageId }));
}
};
}
//# sourceMappingURL=operators.js.map