UNPKG

@kitmi/types

Version:

JavaScript semantic data types

226 lines (181 loc) 6.64 kB
import { InvalidArgument, ValidationError, ApplicationError } from './errors'; let counter = 0; const defaultTypeClasses = []; const defaultPlugins = []; export class TypeSystem { primitives = new Set(); scalarTypes = new Set(); plugins = {}; types = {}; sanitize = this.callByType('sanitize'); sanitize_ = this.callByType('sanitize_'); serialize = this.callByType('serialize'); validate = this.callByType('validate'); constructor() { this._counter = counter++; } static fromDefault() { const ts = new TypeSystem(); defaultTypeClasses.forEach(({ name, TypeMeta }) => { ts.addType(name, TypeMeta); }); defaultPlugins.forEach(({ name, plugin }) => { ts.addPlugin(name, plugin); }); return ts; } addPlugin(name, plugin) { this.plugins[name] = plugin; } removePlugin(name) { delete this.plugins[name]; } _addType(name, typeMeta) { if (name in this.types) { throw new ApplicationError(`Type "${name}" already exist.`, { name }); } this.types[name] = typeMeta; if (typeMeta.primitive) { this.primitives.add(name); } if (typeMeta.scalar) { this.scalarTypes.add(name); } } addType(name, TypeMeta) { const typeMeta = new TypeMeta(this); typeMeta.sanitize = (value, meta, i18n, context) => { meta = { type: typeMeta.name, ...meta }; const opts = { ...(typeof context === 'string' ? { path: context } : context), rawValue: value, i18n, system: this, }; const [isDone, sanitized] = this.beginSanitize(value, meta, opts); return this.endSanitize(isDone ? sanitized : typeMeta._sanitize(value, meta, opts), meta, opts); }; typeMeta.sanitize_ = async (value, meta, i18n, context) => { meta = { type: typeMeta.name, ...meta }; const opts = { ...(typeof context === 'string' ? { path: context } : context), rawValue: value, i18n, system: this, }; const [isDone, sanitized] = await this.beginSanitize(value, meta, opts); return this.endSanitize( isDone ? sanitized : typeMeta._sanitizeAsync ? await typeMeta._sanitizeAsync(value, meta, opts) : typeMeta._sanitize(value, meta, opts), meta, opts ); }; this._addType(name, typeMeta); this._addType(typeMeta.name, typeMeta); typeMeta.alias?.forEach((a) => { this._addType(a, typeMeta); }); } callByType(method) { return (value, typeInfo, i18n, context) => { if (typeInfo.type == null) { throw new InvalidArgument(`Missing type info: ${JSON.stringify(typeInfo)}`); } if (!this.primitives.has(typeInfo.type)) { throw new InvalidArgument(`Unsupported primitive type: "${typeInfo.type}".`); } const typeObject = this.types[typeInfo.type]; return typeObject[method](value, typeInfo, i18n, context); }; } safeJsonStringify(value) { const bigintWriter = this.plugins['bigintWriter']; if (bigintWriter) { const replacer = (_, value) => (typeof value === 'bigint' ? bigintWriter(value) : value); return JSON.stringify(value, replacer); } return JSON.stringify(value); } getStringifier() { const bigintWriter = this.plugins['bigintWriter']; if (bigintWriter) { return (value) => (typeof value === 'bigint' ? bigintWriter(value) : value.toString()); } return null; } beginSanitize(value, meta, opts) { if (value == null) { if (meta.default != null) { return [true, meta.default]; } else if (meta.optional) { return [true, null]; } throw new ValidationError('Missing a required value.', { value: opts.rawValue, path: opts.path, }); } if (meta.const) { if (meta.const !== value) { throw new ValidationError('Invalid constant value.', { value: opts.rawValue, path: opts.path, }); } } if (meta.plain) return [true, value]; // more prerequisites here ... if (this.plugins.preProcess) { return this.plugins.preProcess(value, meta, opts); } return [false]; } endSanitize(value, meta, opts) { if (this.scalarTypes.has(meta.type)) { this.verifyEnum(value, meta, opts); } if (this.plugins.postProcess) { return this.plugins.postProcess(value, meta, opts); } return value; } verifyEnum(value, meta, opts) { if (value == null && meta.optional) return; if (meta.enum && !meta.enum.includes(value)) { throw new ValidationError('Invalid enum value.', { value: opts.rawValue, path: opts.path, }); } } } const defaultTypeSystem = new TypeSystem(); export const addType = (name, TypeMeta) => { defaultTypeSystem.addType(name, TypeMeta); defaultTypeClasses.push({ name, TypeMeta }); }; export const addPlugin = (name, plugin) => { defaultTypeSystem.addPlugin(name, plugin); defaultPlugins.push({ name, plugin }); }; export const createTypeSystem = (emptySystem) => { return emptySystem ? new TypeSystem() : TypeSystem.fromDefault(); }; export const Types = defaultTypeSystem.types; // compatibility Types.sanitize = defaultTypeSystem.sanitize.bind(defaultTypeSystem); Types.sanitize_ = defaultTypeSystem.sanitize_.bind(defaultTypeSystem); Types.serialize = defaultTypeSystem.serialize.bind(defaultTypeSystem); Types.primitives = defaultTypeSystem.primitives; export const charsets = { up_letter_num: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', low_letter_num: '0123456789abcdefghijklmnopqrstuvwxyz', up_letter: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', low_letter: 'abcdefghijklmnopqrstuvwxyz', url_safe_all: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_', }; export default defaultTypeSystem;