mirror of
https://github.com/xuxiaobo-bobo/boda_jsEnv.git
synced 2025-04-20 12:30:02 +08:00
485 lines
17 KiB
JavaScript
485 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) 2020, Peculiar Ventures, All rights reserved.
|
|
*/
|
|
|
|
class JsonError extends Error {
|
|
constructor(message, innerError) {
|
|
super(innerError
|
|
? `${message}. See the inner exception for more details.`
|
|
: message);
|
|
this.message = message;
|
|
this.innerError = innerError;
|
|
}
|
|
}
|
|
|
|
class TransformError extends JsonError {
|
|
constructor(schema, message, innerError) {
|
|
super(message, innerError);
|
|
this.schema = schema;
|
|
}
|
|
}
|
|
|
|
class ParserError extends TransformError {
|
|
constructor(schema, message, innerError) {
|
|
super(schema, `JSON doesn't match to '${schema.target.name}' schema. ${message}`, innerError);
|
|
}
|
|
}
|
|
|
|
class ValidationError extends JsonError {
|
|
}
|
|
|
|
class SerializerError extends JsonError {
|
|
constructor(schemaName, message, innerError) {
|
|
super(`Cannot serialize by '${schemaName}' schema. ${message}`, innerError);
|
|
this.schemaName = schemaName;
|
|
}
|
|
}
|
|
|
|
class KeyError extends ParserError {
|
|
constructor(schema, keys, errors = {}) {
|
|
super(schema, "Some keys doesn't match to schema");
|
|
this.keys = keys;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
|
|
var JsonPropTypes;
|
|
(function (JsonPropTypes) {
|
|
JsonPropTypes[JsonPropTypes["Any"] = 0] = "Any";
|
|
JsonPropTypes[JsonPropTypes["Boolean"] = 1] = "Boolean";
|
|
JsonPropTypes[JsonPropTypes["Number"] = 2] = "Number";
|
|
JsonPropTypes[JsonPropTypes["String"] = 3] = "String";
|
|
})(JsonPropTypes || (JsonPropTypes = {}));
|
|
|
|
function checkType(value, type) {
|
|
switch (type) {
|
|
case JsonPropTypes.Boolean:
|
|
return typeof value === "boolean";
|
|
case JsonPropTypes.Number:
|
|
return typeof value === "number";
|
|
case JsonPropTypes.String:
|
|
return typeof value === "string";
|
|
}
|
|
return true;
|
|
}
|
|
function throwIfTypeIsWrong(value, type) {
|
|
if (!checkType(value, type)) {
|
|
throw new TypeError(`Value must be ${JsonPropTypes[type]}`);
|
|
}
|
|
}
|
|
function isConvertible(target) {
|
|
if (target && target.prototype) {
|
|
if (target.prototype.toJSON && target.prototype.fromJSON) {
|
|
return true;
|
|
}
|
|
else {
|
|
return isConvertible(target.prototype);
|
|
}
|
|
}
|
|
else {
|
|
return !!(target && target.toJSON && target.fromJSON);
|
|
}
|
|
}
|
|
|
|
class JsonSchemaStorage {
|
|
constructor() {
|
|
this.items = new Map();
|
|
}
|
|
has(target) {
|
|
return this.items.has(target) || !!this.findParentSchema(target);
|
|
}
|
|
get(target) {
|
|
const schema = this.items.get(target) || this.findParentSchema(target);
|
|
if (!schema) {
|
|
throw new Error("Cannot get schema for current target");
|
|
}
|
|
return schema;
|
|
}
|
|
create(target) {
|
|
const schema = { names: {} };
|
|
const parentSchema = this.findParentSchema(target);
|
|
if (parentSchema) {
|
|
Object.assign(schema, parentSchema);
|
|
schema.names = {};
|
|
for (const name in parentSchema.names) {
|
|
schema.names[name] = Object.assign({}, parentSchema.names[name]);
|
|
}
|
|
}
|
|
schema.target = target;
|
|
return schema;
|
|
}
|
|
set(target, schema) {
|
|
this.items.set(target, schema);
|
|
return this;
|
|
}
|
|
findParentSchema(target) {
|
|
const parent = target.__proto__;
|
|
if (parent) {
|
|
const schema = this.items.get(parent);
|
|
return schema || this.findParentSchema(parent);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const DEFAULT_SCHEMA = "default";
|
|
const schemaStorage = new JsonSchemaStorage();
|
|
|
|
class PatternValidation {
|
|
constructor(pattern) {
|
|
this.pattern = new RegExp(pattern);
|
|
}
|
|
validate(value) {
|
|
const pattern = new RegExp(this.pattern.source, this.pattern.flags);
|
|
if (typeof value !== "string") {
|
|
throw new ValidationError("Incoming value must be string");
|
|
}
|
|
if (!pattern.exec(value)) {
|
|
throw new ValidationError(`Value doesn't match to pattern '${pattern.toString()}'`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class InclusiveValidation {
|
|
constructor(min = Number.MIN_VALUE, max = Number.MAX_VALUE) {
|
|
this.min = min;
|
|
this.max = max;
|
|
}
|
|
validate(value) {
|
|
throwIfTypeIsWrong(value, JsonPropTypes.Number);
|
|
if (!(this.min <= value && value <= this.max)) {
|
|
const min = this.min === Number.MIN_VALUE ? "MIN" : this.min;
|
|
const max = this.max === Number.MAX_VALUE ? "MAX" : this.max;
|
|
throw new ValidationError(`Value doesn't match to diapason [${min},${max}]`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ExclusiveValidation {
|
|
constructor(min = Number.MIN_VALUE, max = Number.MAX_VALUE) {
|
|
this.min = min;
|
|
this.max = max;
|
|
}
|
|
validate(value) {
|
|
throwIfTypeIsWrong(value, JsonPropTypes.Number);
|
|
if (!(this.min < value && value < this.max)) {
|
|
const min = this.min === Number.MIN_VALUE ? "MIN" : this.min;
|
|
const max = this.max === Number.MAX_VALUE ? "MAX" : this.max;
|
|
throw new ValidationError(`Value doesn't match to diapason (${min},${max})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class LengthValidation {
|
|
constructor(length, minLength, maxLength) {
|
|
this.length = length;
|
|
this.minLength = minLength;
|
|
this.maxLength = maxLength;
|
|
}
|
|
validate(value) {
|
|
if (this.length !== undefined) {
|
|
if (value.length !== this.length) {
|
|
throw new ValidationError(`Value length must be exactly ${this.length}.`);
|
|
}
|
|
return;
|
|
}
|
|
if (this.minLength !== undefined) {
|
|
if (value.length < this.minLength) {
|
|
throw new ValidationError(`Value length must be more than ${this.minLength}.`);
|
|
}
|
|
}
|
|
if (this.maxLength !== undefined) {
|
|
if (value.length > this.maxLength) {
|
|
throw new ValidationError(`Value length must be less than ${this.maxLength}.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class EnumerationValidation {
|
|
constructor(enumeration) {
|
|
this.enumeration = enumeration;
|
|
}
|
|
validate(value) {
|
|
throwIfTypeIsWrong(value, JsonPropTypes.String);
|
|
if (!this.enumeration.includes(value)) {
|
|
throw new ValidationError(`Value must be one of ${this.enumeration.map((v) => `'${v}'`).join(", ")}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
class JsonTransform {
|
|
static checkValues(data, schemaItem) {
|
|
const values = Array.isArray(data) ? data : [data];
|
|
for (const value of values) {
|
|
for (const validation of schemaItem.validations) {
|
|
if (validation instanceof LengthValidation && schemaItem.repeated) {
|
|
validation.validate(data);
|
|
}
|
|
else {
|
|
validation.validate(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static checkTypes(value, schemaItem) {
|
|
if (schemaItem.repeated && !Array.isArray(value)) {
|
|
throw new TypeError("Value must be Array");
|
|
}
|
|
if (typeof schemaItem.type === "number") {
|
|
const values = Array.isArray(value) ? value : [value];
|
|
for (const v of values) {
|
|
throwIfTypeIsWrong(v, schemaItem.type);
|
|
}
|
|
}
|
|
}
|
|
static getSchemaByName(schema, name = DEFAULT_SCHEMA) {
|
|
return { ...schema.names[DEFAULT_SCHEMA], ...schema.names[name] };
|
|
}
|
|
}
|
|
|
|
class JsonSerializer extends JsonTransform {
|
|
static serialize(obj, options, replacer, space) {
|
|
const json = this.toJSON(obj, options);
|
|
return JSON.stringify(json, replacer, space);
|
|
}
|
|
static toJSON(obj, options = {}) {
|
|
let res;
|
|
let targetSchema = options.targetSchema;
|
|
const schemaName = options.schemaName || DEFAULT_SCHEMA;
|
|
if (isConvertible(obj)) {
|
|
return obj.toJSON();
|
|
}
|
|
if (Array.isArray(obj)) {
|
|
res = [];
|
|
for (const item of obj) {
|
|
res.push(this.toJSON(item, options));
|
|
}
|
|
}
|
|
else if (typeof obj === "object") {
|
|
if (targetSchema && !schemaStorage.has(targetSchema)) {
|
|
throw new JsonError("Cannot get schema for `targetSchema` param");
|
|
}
|
|
targetSchema = (targetSchema || obj.constructor);
|
|
if (schemaStorage.has(targetSchema)) {
|
|
const schema = schemaStorage.get(targetSchema);
|
|
res = {};
|
|
const namedSchema = this.getSchemaByName(schema, schemaName);
|
|
for (const key in namedSchema) {
|
|
try {
|
|
const item = namedSchema[key];
|
|
const objItem = obj[key];
|
|
let value;
|
|
if ((item.optional && objItem === undefined)
|
|
|| (item.defaultValue !== undefined && objItem === item.defaultValue)) {
|
|
continue;
|
|
}
|
|
if (!item.optional && objItem === undefined) {
|
|
throw new SerializerError(targetSchema.name, `Property '${key}' is required.`);
|
|
}
|
|
if (typeof item.type === "number") {
|
|
if (item.converter) {
|
|
if (item.repeated) {
|
|
value = objItem.map((el) => item.converter.toJSON(el, obj));
|
|
}
|
|
else {
|
|
value = item.converter.toJSON(objItem, obj);
|
|
}
|
|
}
|
|
else {
|
|
value = objItem;
|
|
}
|
|
}
|
|
else {
|
|
if (item.repeated) {
|
|
value = objItem.map((el) => this.toJSON(el, { schemaName }));
|
|
}
|
|
else {
|
|
value = this.toJSON(objItem, { schemaName });
|
|
}
|
|
}
|
|
this.checkTypes(value, item);
|
|
this.checkValues(value, item);
|
|
res[item.name || key] = value;
|
|
}
|
|
catch (e) {
|
|
if (e instanceof SerializerError) {
|
|
throw e;
|
|
}
|
|
else {
|
|
throw new SerializerError(schema.target.name, `Property '${key}' is wrong. ${e.message}`, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
res = {};
|
|
for (const key in obj) {
|
|
res[key] = this.toJSON(obj[key], { schemaName });
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
res = obj;
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
class JsonParser extends JsonTransform {
|
|
static parse(data, options) {
|
|
const obj = JSON.parse(data);
|
|
return this.fromJSON(obj, options);
|
|
}
|
|
static fromJSON(target, options) {
|
|
const targetSchema = options.targetSchema;
|
|
const schemaName = options.schemaName || DEFAULT_SCHEMA;
|
|
const obj = new targetSchema();
|
|
if (isConvertible(obj)) {
|
|
return obj.fromJSON(target);
|
|
}
|
|
const schema = schemaStorage.get(targetSchema);
|
|
const namedSchema = this.getSchemaByName(schema, schemaName);
|
|
const keyErrors = {};
|
|
if (options.strictProperty && !Array.isArray(target)) {
|
|
JsonParser.checkStrictProperty(target, namedSchema, schema);
|
|
}
|
|
for (const key in namedSchema) {
|
|
try {
|
|
const item = namedSchema[key];
|
|
const name = item.name || key;
|
|
const value = target[name];
|
|
if (value === undefined && (item.optional || item.defaultValue !== undefined)) {
|
|
continue;
|
|
}
|
|
if (!item.optional && value === undefined) {
|
|
throw new ParserError(schema, `Property '${name}' is required.`);
|
|
}
|
|
this.checkTypes(value, item);
|
|
this.checkValues(value, item);
|
|
if (typeof (item.type) === "number") {
|
|
if (item.converter) {
|
|
if (item.repeated) {
|
|
obj[key] = value.map((el) => item.converter.fromJSON(el, obj));
|
|
}
|
|
else {
|
|
obj[key] = item.converter.fromJSON(value, obj);
|
|
}
|
|
}
|
|
else {
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
else {
|
|
const newOptions = {
|
|
...options,
|
|
targetSchema: item.type,
|
|
schemaName,
|
|
};
|
|
if (item.repeated) {
|
|
obj[key] = value.map((el) => this.fromJSON(el, newOptions));
|
|
}
|
|
else {
|
|
obj[key] = this.fromJSON(value, newOptions);
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
if (!(e instanceof ParserError)) {
|
|
e = new ParserError(schema, `Property '${key}' is wrong. ${e.message}`, e);
|
|
}
|
|
if (options.strictAllKeys) {
|
|
keyErrors[key] = e;
|
|
}
|
|
else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
const keys = Object.keys(keyErrors);
|
|
if (keys.length) {
|
|
throw new KeyError(schema, keys, keyErrors);
|
|
}
|
|
return obj;
|
|
}
|
|
static checkStrictProperty(target, namedSchema, schema) {
|
|
const jsonProps = Object.keys(target);
|
|
const schemaProps = Object.keys(namedSchema);
|
|
const keys = [];
|
|
for (const key of jsonProps) {
|
|
if (schemaProps.indexOf(key) === -1) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
if (keys.length) {
|
|
throw new KeyError(schema, keys);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getValidations(item) {
|
|
const validations = [];
|
|
if (item.pattern) {
|
|
validations.push(new PatternValidation(item.pattern));
|
|
}
|
|
if (item.type === JsonPropTypes.Number || item.type === JsonPropTypes.Any) {
|
|
if (item.minInclusive !== undefined || item.maxInclusive !== undefined) {
|
|
validations.push(new InclusiveValidation(item.minInclusive, item.maxInclusive));
|
|
}
|
|
if (item.minExclusive !== undefined || item.maxExclusive !== undefined) {
|
|
validations.push(new ExclusiveValidation(item.minExclusive, item.maxExclusive));
|
|
}
|
|
if (item.enumeration !== undefined) {
|
|
validations.push(new EnumerationValidation(item.enumeration));
|
|
}
|
|
}
|
|
if (item.type === JsonPropTypes.String || item.repeated || item.type === JsonPropTypes.Any) {
|
|
if (item.length !== undefined || item.minLength !== undefined || item.maxLength !== undefined) {
|
|
validations.push(new LengthValidation(item.length, item.minLength, item.maxLength));
|
|
}
|
|
}
|
|
return validations;
|
|
}
|
|
const JsonProp = (options = {}) => (target, propertyKey) => {
|
|
const errorMessage = `Cannot set type for ${propertyKey} property of ${target.constructor.name} schema`;
|
|
let schema;
|
|
if (!schemaStorage.has(target.constructor)) {
|
|
schema = schemaStorage.create(target.constructor);
|
|
schemaStorage.set(target.constructor, schema);
|
|
}
|
|
else {
|
|
schema = schemaStorage.get(target.constructor);
|
|
if (schema.target !== target.constructor) {
|
|
schema = schemaStorage.create(target.constructor);
|
|
schemaStorage.set(target.constructor, schema);
|
|
}
|
|
}
|
|
const defaultSchema = {
|
|
type: JsonPropTypes.Any,
|
|
validations: [],
|
|
};
|
|
const copyOptions = Object.assign(defaultSchema, options);
|
|
copyOptions.validations = getValidations(copyOptions);
|
|
if (typeof copyOptions.type !== "number") {
|
|
if (!schemaStorage.has(copyOptions.type) && !isConvertible(copyOptions.type)) {
|
|
throw new Error(`${errorMessage}. Assigning type doesn't have schema.`);
|
|
}
|
|
}
|
|
let schemaNames;
|
|
if (Array.isArray(options.schema)) {
|
|
schemaNames = options.schema;
|
|
}
|
|
else {
|
|
schemaNames = [options.schema || DEFAULT_SCHEMA];
|
|
}
|
|
for (const schemaName of schemaNames) {
|
|
if (!schema.names[schemaName]) {
|
|
schema.names[schemaName] = {};
|
|
}
|
|
const namedSchema = schema.names[schemaName];
|
|
namedSchema[propertyKey] = copyOptions;
|
|
}
|
|
};
|
|
|
|
export { JsonError, JsonParser, JsonProp, JsonPropTypes, JsonSerializer, KeyError, ParserError, SerializerError, TransformError, ValidationError };
|