@barchart/common-js
Version:
Library of common JavaScript utilities
334 lines (270 loc) • 7.13 kB
JavaScript
const attributes = require('./../../lang/attributes'),
functions = require('./../../lang/functions'),
is = require('./../../lang/is');
const LinkedList = require('./../../collections/LinkedList'),
Tree = require('./../../collections/Tree');
const Component = require('./Component'),
Field = require('./Field');
module.exports = (() => {
'use strict';
/**
* A schema definition, can be used for serialization and deserialization.
*
* @public
* @param {String} name - The name of the schema
* @param {Field[]} fields
* @param {Component[]} components
* @param {Boolean=} strict
*/
class Schema {
constructor(name, fields, components, strict) {
this._name = name;
this._fields = fields || [ ];
this._components = components || [ ];
this._strict = is.boolean(strict) && strict;
this._revivers = getReviverItems(this._fields, this._components);
}
/**
* Accepts data and returns a new object which (should) conform to
* the schema.
*
* @public
* @param {Object} data
* @returns {Object}
*/
format(data) {
const returnRef = { };
this._fields.forEach((field) => {
formatField(returnRef, field, data);
});
this._components.forEach((component) => {
component.fields.forEach((field) => {
formatField(returnRef, field, data);
});
});
return returnRef;
}
/**
* Name of the table.
*
* @public
* @returns {String}
*/
get name() {
return this._name;
}
/**
* The fields of the table.
*
* @public
* @returns {Array<Field>}
*/
get fields() {
return [...this._fields];
}
/**
* The components of the table.
*
* @public
* @returns {Array<Component>}
*/
get components() {
return [...this._components];
}
/**
* If true, only the explicitly defined fields and components will
* be serialized.
*
* @public
* @returns {boolean}
*/
get strict() {
return this._strict;
}
/**
* Returns true, if an object complies with the schema.
*
* @public
* @param {*} candidate
* @returns {Boolean}
*/
validate(candidate) {
return !getCandidateIsInvalid(candidate) && this.getInvalidFields(candidate).length === 0;
}
/**
* Returns an array of {@link Field} objects from the schema for which the
* candidate object does not comply with.
*
* @public
* @param {*} candidate
* @returns {Field[]}
*/
getInvalidFields(candidate) {
if (getCandidateIsInvalid(candidate)) {
return this.fields.filter(f => !f.optional);
}
return this.fields.reduce((problems, field) => {
let check = !field.optional || attributes.has(candidate, field.name);
if (check) {
const valid = field.dataType.validator.call(this, attributes.read(candidate, field.name));
if (!valid) {
problems.push(field);
}
}
return problems;
}, [ ]);
}
/**
* Generates a function suitable for use by {@link JSON.parse}.
*
* @public
* @returns {Function}
*/
getReviver() {
let head = this._revivers;
let node = null;
const advance = (key) => {
const previous = node;
if (node === null) {
node = head;
} else {
node = node.getNext();
}
const item = node.getValue();
if (key === item.name) {
return item;
} else if (item.reset || (key === '' && node === head)) {
node = null;
return item;
} else if (item.array && is.integer(parseInt(key))) {
node = previous;
return item;
} else if (item.optional) {
return advance(key);
} else {
throw new SchemaError(key, item.name, `Schema parsing is using strict mode, unexpected key found [ found: ${key}, expected: ${item.name} ]`);
}
};
return (key, value) => {
const item = advance(key);
if (key === '' || (item.array && key === item.name)) {
return value;
} else {
return item.reviver(value);
}
};
}
/**
* Returns a function that will generate a *new* reviver function
* (see {@link Schema#getReviver}.
*
* @public
* @returns {Function}
*/
getReviverFactory() {
return () => this.getReviver();
}
toString() {
return `[Schema (name=${this._name})]`;
}
}
class SchemaError extends Error {
constructor(key, name, message) {
super(message);
this.key = key;
this.name = name;
}
toString() {
return `[SchemaError]`;
}
}
class ReviverItem {
constructor(name, reviver, optional, reset, array) {
this._name = name;
this._reviver = reviver || functions.getTautology();
this._optional = is.boolean(optional) && optional;
this._reset = is.boolean(reset) && reset;
this._array = is.boolean(array) && array;
}
get name() {
return this._name;
}
get reviver() {
return this._reviver;
}
get optional() {
return this._optional;
}
get reset() {
return this._reset;
}
get array() {
return this._array;
}
}
function getReviverItems(fields, components) {
const root = new Tree(new ReviverItem(null, null, false, true));
// 2017/08/26, BRI. The Field and Component types could inherit a common
// type, allowing the following duplication to be avoided with polymorphism.
fields.forEach((field) => {
const names = field.name.split('.');
let node = root;
names.forEach((name, i) => {
if (names.length === i + 1) {
node.addChild(new ReviverItem(name, field.dataType.reviver, field.optional, false, field.array));
} else {
let child = node.findChild(n => n.name === name);
if (!child) {
child = node.addChild(new ReviverItem(name));
}
node = child;
}
});
});
components.forEach((component) => {
let node = root;
const names = component.name.split('.');
names.forEach((name, i) => {
if (names.length === i + 1) {
node = node.addChild(new ReviverItem(name, component.reviver));
} else {
let child = node.findChild(n => n.name === name);
if (!child) {
child = node.addChild(new ReviverItem(name));
}
node = child;
}
});
component.fields.forEach((f) => node.addChild(new ReviverItem(f.name, f.dataType.reviver)));
});
let head = null;
let current = null;
const addItemToList = (item, node) => {
let itemToUse = item;
if (!node.getIsLeaf()) {
const required = node.search((i, n) => n.getIsLeaf() && !i.optional, true, false) !== null;
if (!required) {
itemToUse = new ReviverItem(item.name, item.reviver, true, item.reset, item.array);
}
} else {
itemToUse = item;
}
if (current === null) {
current = head = new LinkedList(itemToUse);
} else {
current = current.insert(itemToUse);
}
};
root.walk(addItemToList, false, true);
return head;
}
function formatField(target, field, data) {
if (attributes.has(data, field.name)) {
attributes.write(target, field.name, field.dataType.convert(attributes.read(data, field.name)));
}
}
function getCandidateIsInvalid(candidate) {
return is.undefined(candidate) || is.null(candidate) || !is.object(candidate);
}
return Schema;
})();