Skip to content

Instantly share code, notes, and snippets.

@psych0der
Created August 10, 2018 11:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save psych0der/4e9e83273577c86dea72cf6d3179995b to your computer and use it in GitHub Desktop.
Save psych0der/4e9e83273577c86dea72cf6d3179995b to your computer and use it in GitHub Desktop.
StylusSDK: Writing platform helper for Juggernaut writing platform
/**
* SSDK: Stylus SDK
* Wraps around editor instance used in app and manages interaction with API. This is basically
* taking care of persisting/caching changes made and communicating with API.
*
* Exposes 2 interfaces for the editor
*
* HTML ==> VDOM
* API specific JSON ==> HTML
*
* Assumptions for the editor:
* - Should be able to spit out HTML of the text entered
* - Be able to load pre-generated HTML
*/
/* ES6 imports */
import {
copyProps, hasAttr, getAttr
}
from '../../utils';
/* Requires Virtual DOM for interacting with HTMl changes*/
let VNode = require('virtual-dom/vnode/vnode'),
VText = require('virtual-dom/vnode/vtext'),
diff = require('virtual-dom/diff'),
createElement = require('virtual-dom/create-element'),
md5 = require('blueimp-md5/js/md5.min.js');
class StylusSDK {
constructor(bookId = null, facade = null, localStorageInterface = null,
Plugins = {}, modifiers = {}) {
if (bookId === null || facade === null || localStorageInterface === null) {
/* Return error */
throw new Error('Required parameters are missing ....');
}
/* html-to-vdom is a bridge to directly convert html to virtual-dom object*/
this._convertHTMLToVDOM = require('html-to-vdom')({
VNode: VNode,
VText: VText
});
/* book_id of book being edited */
this._bookId = bookId;
/* facade isntance to be used */
this._facade = facade;
/* localStorage instance for persistence*/
this._localStorage = localStorageInterface;
/* counter of last version pushed */
this._last_version_uploaded = null;
/* resolve method reference for internal state promise */
this._resolve = null;
/* reject method reference for internal state promise */
this._reject = null;
/* internal promise to keep track of loaded state */
this._promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
/* VDOM to html converter */
this._convertVDOMToHTML = createElement;
/* Declare private instance of html */
this._HTML = '';
/* current VDOM version */
this._current_StylusOBJ_version = -1;
/* StylusOBJ prototype to create new instances */
this._StylusOBJPrototype = null;
/* current Stylus object*/
this._current_SylusOBJ = null;
/* current serialized StylusOBJ */
this._serializedStylusOBJ = '';
/* Dictionary of stylusOBJ objects with meta data such as creation time */
this._stylusOBJContainer = {};
/* Boolean switch to single freezing of plugins */
this._pluginsLoaded = false;
/* Plugin registry of SSDK */
this._plugins = {};
/* Boolean for signifying init status */
this._loaded = false;
/* apply modifiers only in case of development */
this._modifiers = (__PRODUCTION__) ? {} : modifiers;
/* load plugins */
this._loadPlugins(Plugins);
/* initialize plugin */
this._init();
}
/* Utility to create and persist stylus index */
_persistStylusIndex() {
let newStylusIndex = Object.assign({}, {
currentVersion: this._current_StylusOBJ_version,
stylusOBJContainer: this._stylusOBJContainer
});
return this._localStorage.set('STYLUS_INDEX_' + this._bookId,
newStylusIndex);
}
/**
* Removes previous stylusOBJ versions from localstorage
*/
removePreviousStylusOBJVersions(version) {
let targetVersion = parseInt(version);
Object.keys(this._stylusOBJContainer).forEach((key) => {
let sourceVersion = parseInt(key);
if(sourceVersion <= targetVersion && sourceVersion != parseInt(this._current_StylusOBJ_version)){
delete this._stylusOBJContainer[key];
}
});
this._persistStylusIndex();
}
/**
* Pushes the unsynced versions present on localmachine to server
*/
pushPendingStylusOBJVersions(upstream_version) {
let targetVersion = parseInt(upstream_version);
Object.keys(this._stylusOBJContainer).forEach((key) => {
let sourceVersion = parseInt(key);
if(sourceVersion > targetVersion) {
/* Pusing previous data to blotter*/
this._facade({
eventName: 'pushToSocket',
arguments: {
'event': 'upload',
'eventPayload': {
requestId: key,
payload: {
bookId: this._bookId,
version: key,
md5: this._stylusOBJContainer[key]['md5'],
timestamp: this._stylusOBJContainer[key]['timestamp'],
stylusOBJ: this._stylusOBJContainer[key]['stylusOBJ']
}
}
}
});
}
})
}
/**
* Loads plugins specifid in Plugins object
* @param {object} Plugins object containing individual plugins
*/
_loadPlugins(Plugins) {
if (this._pluginsLoaded) {
throw 'Plugins already loaded. SSDK instance can\'t be modified';
}
/* typechecking for plugins */
if (typeof Plugins !== 'object') {
console.error('Invalid object provided for plugins')
return false;
}
this._pluginsLoaded = true;
/* load plugins */
for (let indentifier in Plugins) {
let plugin = Plugins[indentifier];
/* TODO: add type checking for plugin object */
/* creating type if doesn't exist */
if (!(plugin.type in this._plugins)) {
this._plugins[plugin.type] = [];
}
/* registering plugins in SDK*/
this._plugins[plugin.type].push(plugin.plugin);
}
}
/**
* StylusOBJ Initializer
* Tries to fetch data from localstorage and validates it.
* If the data from localStorage passes all tests, It'll be assigned to private instances
*/
_init() {
/* attach event handler to socket events */
this._facade({
'eventName': 'attachSocketEventHandler',
arguments: {
'event': 'bookVersionStatus',
'callback': {
'key': 'bookVersionStatus',
'function': (payload) => {
payload = JSON.parse(payload);
if('status' in payload){
if(payload['status'] === 'success') {
this.removePreviousStylusOBJVersions(payload.version);
}
}
}
}
}
});
``
let StylusIndex = this._localStorage.get('STYLUS_INDEX_' + this._bookId);
let state = null;
let recoveredHash = null;
let recoveredTimestamp = null;
if (StylusIndex === null || StylusIndex === {} ||
!(hasAttr(StylusIndex, 'currentVersion')) ||
!(hasAttr(StylusIndex, 'stylusOBJContainer'))
) {
/* No Stylus state found. Assuming fresh start */
state = false;
} else {
let currentVersion = StylusIndex.currentVersion;
let stylusOBJContainer = StylusIndex.stylusOBJContainer;
/* extracting current version from stylusOBJContainer to currentStylusOBJ */
if (currentVersion > -1) {
/* if current version is not available in localstorage, reset localstorage */
if (!(currentVersion in stylusOBJContainer)) {
/* no meaningful data contained */
state = false;
}
let currentStylusOBJ = ('stylusOBJ' in stylusOBJContainer[
currentVersion]) ? stylusOBJContainer[currentVersion].stylusOBJ :
null;
let response = this._validateStylusOBJ(currentStylusOBJ);
if (!response.status) {
/* Stylus OBJ recovered is corrupted */
state = false;
}
recoveredHash = ('md5' in stylusOBJContainer[currentVersion]) ?
StylusIndex.stylusOBJContainer[currentVersion].md5 : null;
recoveredTimestamp = ('timestamp' in stylusOBJContainer[
currentVersion]) ? StylusIndex.stylusOBJContainer[currentVersion]
.timestamp : null;
if (recoveredTimestamp === null) {
/* couldn't fetch meta data from localstorage data. Reset the localstorage */
state = false;
}
if (recoveredHash === null) {
/* couldn't fetch meta data from localstorage data. Reset the localstorage */
state = false;
}
let hash = md5(JSON.stringify(currentStylusOBJ));
/* check whether targetHash matches hash of stored stylus */
if (hash != recoveredHash) {
/* corrupted state found. Reset it */
state = false;
} else {
state = true;
this._current_StylusOBJ_version = currentVersion;
this._stylusOBJContainer = stylusOBJContainer;
this._current_SylusOBJ = currentStylusOBJ;
}
} else {
/* No meaningful data can be recovered from localstorage */
state = false;
}
}
/* if state is corrupted, reinitalize the state */
this._persistStylusIndex();
/* Don't connect with server if no reconcile is passed [DEVELOPMENT mode only] */
if (('NO_RECONCILE' in this._modifiers && this._modifiers['NO_RECONCILE'] ===
true)) {
this._loaded = true;
return this._resolve({
'status': 'success',
'message': 'SSDK is ready to serve'
});
}
/* fire the call to check for the latest version */
this._facade({
eventName: 'fetch',
arguments: {
reqName: 'getLatestState',
urlContext: {
'bookId': this._bookId
},
options: {},
successCallback: (response) => {
let responseData = response.data;
let fetchLatestVersion = false;
if (!(responseData && responseData.payload)) {
// todo : no data found
fetchLatestVersion = false;
} else {
let current_version_data = responseData.payload[
'current_version'];
let upstream_timestamp = current_version_data['timestamp'];
let upstream_md5 = current_version_data['md5']
let upstream_version = current_version_data['version'];
/* checking in case of legitimate data has been recovered from localstorage */
if (state) {
if (upstream_version > this._current_StylusOBJ_version) {
fetchLatestVersion = true;
} else {
if (upstream_version < this._current_StylusOBJ_version) {
this.pushPendingStylusOBJVersions(upstream_version);
} else {
if ((upstream_md5 !== recoveredHash) || (
recoveredTimestamp !== upstream_timestamp)) {
fetchLatestVersion = true;
}
}
}
} else {
if (upstream_version > -1)
fetchLatestVersion = true;
}
}
if (fetchLatestVersion) {
this._facade({
'eventName': 'fetch',
'arguments': {
reqName: 'getLatestVersion',
urlContext: {
'bookId': this._bookId
},
options: {},
successCallback: (response) => {
let responseData = response.data;
if (!(responseData && responseData.payload)) {
// todo : no data found
fetchLatestVersion = false;
} else {
let data = responseData.payload;
this._current_StylusOBJ_version = data[
'version'];
this._current_SylusOBJ = data['stylusOBJ'];
/* pushing fetched data to stylusOBJ container */
this._stylusOBJContainer[this._current_StylusOBJ_version] = {
version: this._current_StylusOBJ_version,
stylusOBJ: Object.assign({}, this._current_SylusOBJ),
timestamp: data.timestamp,
md5: data.md5
};
/* saving state in localstorage */
this._persistStylusIndex();
this._loaded = true;
this._resolve({
'status': 'success',
'message': 'SSDK is ready to serve'
});
}
}
}
})
} else {
this._loaded = true;
this._resolve({
'status': 'success',
'message': 'SSDK is ready to serve'
});
}
},
errorCallback: (err) => {
console.error('error while communicating with server ');
console.error(err);
this._reject({
'status': 'failed',
'message': 'unable to reconcile with server',
'error': err
});
}
}
});
}
/**
* Validates VDOM to custom rules/requirements for StylusOBJ
* Current criteria for checking:
* - every node should have:
* - data-type attribute
* VirtualText should have parent with data-leaf attribute
*
*/
_validateVDOM(VDOM, parent = null) {
let errors = [];
let required_attrs = ['properties.attributes.data-type',
'properties.attributes.data-id'
];
let leafAttr = 'properties.attributes.data-leaf';
/* return error if individual elements are returned */
if (Array.isArray(VDOM)) {
return {
status: false,
errors: ["Sections aren't contained in parent element"]
};
}
/* check for all required attrs */
for (let attr of required_attrs) {
if ((getAttr(VDOM, attr) === undefined)) {
return {
status: false,
errors: ["doesn't contain required attr: " + attr]
};
}
}
if (VDOM.type == 'VirtualText') {
if (getAttr(parent, leafAttr) !== "true") {
return {
status: false,
errors: ['Text node is not bounded by data leaf parent']
};
}
} else {
if (getAttr(VDOM, leafAttr) === "true") {
/* break loop and return if node is data-leaf */
return {
status: true,
errors: []
};
} else {
let childrenStatus = true;
/* validate each child */
VDOM.children.forEach((child) => {
let response = this._validateVDOM(child, VDOM);
childrenStatus = childrenStatus && response.status;
errors = errors.concat(response.errors);
});
return {
status: childrenStatus,
errors: errors
};
}
}
}
/**
* Validates stylusOBJ
* Current criteria for checking:
* - every object should have:
* - data-type attribute in props
* data-leaf attributes in bottom elements
* data-leaf elements should have html elements
*
*/
_validateStylusOBJ(stylusOBJ, parent = null) {
let errors = [];
let required_attrs = ['type', 'tagName', 'isLeaf', 'meta',
'properties.attributes.data-type', 'properties.attributes.data-id'
];
let leafAttr = 'properties.attributes.data-leaf';
/* check for all required attrs */
for (let attr of required_attrs) {
if ((getAttr(stylusOBJ, attr) === undefined)) {
return {
status: false,
errors: ["doesn't contain required attr: " + attr]
};
}
}
/* check if both isLeaf and data-leaf are not consistent */
if ((stylusOBJ.isLeaf && getAttr(stylusOBJ, leafAttr) == undefined) || (!
stylusOBJ.isLeaf && getAttr(stylusOBJ, leafAttr) == "true")) {
return {
status: false,
errors: ['Leaf attribute is not consistent']
};
}
/* Check for HTML attribute in leaf elements */
if (stylusOBJ.isLeaf) {
if (getAttr(stylusOBJ, 'HTML') === undefined || getAttr(stylusOBJ,
'HTML') === "") {
return {
status: false,
errors: ['Leaf node doesn\'nt contain HTML attribute']
};
}
/* break loop and return if node is leaf */
return {
status: true,
errors: []
};
} else {
if (stylusOBJ.children.length == 0) {
/* terminal obect is not labeled as leaf node */
return {
status: false,
errors: ['Terminal node is not labeled as leaf']
};
}
let childrenStatus = true;
/* validate each child */
stylusOBJ.children.forEach((child) => {
let response = this._validateStylusOBJ(child, stylusOBJ);
childrenStatus = childrenStatus && response.status;
errors = errors.concat(response.errors);
});
return {
status: childrenStatus,
errors: errors
};
}
}
/**
* Prepare VDOM from current HTML
*/
_prepareVDOM(HTML) {
let VDOM = this._convertHTMLToVDOM(HTML);
let response = this._validateVDOM(VDOM);
if (!response.status) {
return {
'status': response.status,
'message': 'Error in parsing HTML',
errors: response.errors
}
}
return {
'status': response.status,
'VDOM': VDOM
};
}
/**
* Returns private instance of current VDOM
*/
getVDOM() {
return this._current_VDOM;
}
/**
* Load StylusOBJ from JSON
*/
loadStylusOBJ(serializedStylusOBJ) {
this._setSerializedStylusOBJ(serializedStylusOBJ);
/* TODO: VALIDATE StylusOBJ */
this._current_SylusOBJ = JSON.parse(this._serializedStylusOBJ);
}
/**
* set serialized StylusOBJ string
*/
_setSerializedStylusOBJ(StylusOBJString) {
this._serializedStylusOBJ = StylusOBJString;
}
/**
* Factory function for StylusOBJ
*/
_getNewStylusOBJ() {
if (this._StylusOBJPrototype === null) {
let StylusOBJ = {};
/* creating attributes for Stylus OBJ */
StylusOBJ.type = "";
StylusOBJ.properties = null;
StylusOBJ.children = [];
StylusOBJ.meta = {};
/* setting StylusOBJ to prototype */
this._StylusOBJPrototype = StylusOBJ;
}
return Object.assign({}, this._StylusOBJPrototype);
}
_getMapperObject(mapPair) {
/* TODO: add type checking and index count checking [should have 2 attributes] */
return {
'from': mapPair[0],
'to': mapPair[1]
}
}
_getMapperRegistryFromTupples(Tupples) {
/* TODO: add type checking for type [array of array] */
let mapperRegistry = Tupples.map(Tupple => this._getMapperObject(Tupple));
return mapperRegistry;
}
/**
* Maps attributes from VDOM to StylusOBJ
*/
_assignStylusOBJAttrs(StylusOBJ, VDOM) {
/* This syntax will be used to transfer property from source object to target object */
let mapperTupples = [
['properties', 'properties'],
['tagName', 'tagName']
];
let mapperRegistry = this._getMapperRegistryFromTupples(mapperTupples);
/* assigning attributes to StylusOBJ */
StylusOBJ = copyProps(VDOM, StylusOBJ, mapperRegistry);
}
/**
* Attached correct type to StylusOBJ based on VDOM
*/
_assignStylusOBJType(StylusOBJ, VDOM) {
/* copy attrs to base level */
StylusOBJ.type = VDOM.properties.attributes['data-type'];
StylusOBJ.section_id = VDOM.properties.attributes['data-id'];
/* assign isLeaf attribute to StylusOBJ if */
if ('data-leaf' in StylusOBJ.properties.attributes && StylusOBJ.properties
.attributes['data-leaf'] == "true") {
StylusOBJ.isLeaf = true;
} else {
StylusOBJ.isLeaf = false;
}
}
/**
* Apply plugins on StylusOBJ to add meta information on objects
*/
_postProcessStylusOBJ(StylusOBJ) {
/* apply plugins to StylusOBJ */
if (StylusOBJ.type in this._plugins) {
this._plugins[StylusOBJ.type].forEach((plugin) => {
let so = JSON.parse(JSON.stringify(StylusOBJ)); // clone StylusOBJ to prevent side effects
Object.freeze(so);
let {assets, ...extraMeta} = plugin(so);
/* merging meta with StylusOBJ */
StylusOBJ.meta = Object.assign({}, StylusOBJ.meta, extraMeta);
StylusOBJ.meta.assets = Object.assign({}, StylusOBJ.meta.assets, assets);
})
}
}
/**
* Function to decodes html string.
* Currently removes `amp;` to fix image urls
*/
_scrubHTMLText(html) {
let entitiesToRemove = ['amp;']
let processString = html;
entitiesToRemove.forEach(token => {
html = html.replace(new RegExp(token, 'g'), '');
});
return html;
}
/**
* converts VDOM to proprietary StylusOBJ
*/
_convertVDOMToStylusOBJ(VDOM) {
let StylusOBJ = this._getNewStylusOBJ();
this._assignStylusOBJAttrs(StylusOBJ, VDOM);
this._assignStylusOBJType(StylusOBJ, VDOM);
/* prepare children StylusObJ if this is not a LEAF object */
if (!StylusOBJ.isLeaf) {
StylusOBJ.children = VDOM.children.map(child => this._convertVDOMToStylusOBJ(
child))
} else {
/* add html to StylusOBJ if it is a leaf */
let DOMElement = this._convertVDOMToHTML(VDOM);
StylusOBJ.HTML = this._scrubHTMLText(DOMElement.outerHTML);
}
/* execute plugins on StylusOBJ */
this._postProcessStylusOBJ(StylusOBJ);
return StylusOBJ;
}
/**
* Prepares StylusOBJ by converting specifid VDOM to the proprietary format
*/
_prepareStylusObject(VDOM) {
/* exit if current VDOM is null */
if (VDOM === null) {
return false;
}
let ParentStylusOBJ = {};
/* Convert root VDOM to StylusOBJ */
ParentStylusOBJ = this._convertVDOMToStylusOBJ(VDOM);
return ParentStylusOBJ;
}
/**
* generate inline styles from StylusOBJ
*/
_getInlineStyleFromStylusObject(StylusOBJ) {
let styles = [];
let StylusOBJStyles = StylusOBJ.properties.styles;
for (let key in StylusOBJStyles) {
if (StylusOBJStyles.hasOwnProperty(key)) {
styles.push(key + ":" + StylusOBJStyles[key]);
}
}
return "style=" + '"' + styles.join(';') + '"';
}
/**
* Generate attributes from StylusOBJ
*/
_getHTMLAttributesFromStylusObject(StylusOBJ) {
let attributes = []; // container to store attributes
let attrs = StylusOBJ.properties.attributes;
for (let key in attrs) {
if (attrs.hasOwnProperty(key)) {
attributes.push(key + '="' + attrs[key] + '"');
}
}
/* push style attributes */
let styles = this._getInlineStyleFromStylusObject(StylusOBJ);
attributes.push(styles);
return attributes.join(' ');
}
/**
* Convert StylusOBJ to representative HTML string
*/
_convertStylusOBJToHTML(StylusOBJ) {
let HTMLContainer = []; /* array containing html tags which will be concatenated later */
/* If object is leaf then return the stored html */
if (StylusOBJ.isLeaf) {
return StylusOBJ.HTML;
}
/* In case of non leaf element generate html */
let attributes = this._getHTMLAttributesFromStylusObject(StylusOBJ);
/* create opening tag of this element */
let HTMLString = '<' + StylusOBJ.tagName + ' ' + attributes + ' >'
HTMLContainer.push(HTMLString);
/* Generate html of children */
StylusOBJ.children.forEach(child => {
HTMLContainer.push(this._convertStylusOBJToHTML(child));
});
/* add closing tag of this object */
HTMLContainer.push('</' + StylusOBJ.tagName + '>');
return HTMLContainer.join('');
}
/**
* Set html
*/
setHTML(HTML) {
if (!this._loaded) {
return {
'status': 'false',
'message': 'SSDK has not finished loading data ....',
'error': 'SSDK_NOT_READY'
};
}
if (!HTML) {
return {
'status': 'failed',
'message': 'empty html string provided',
'error': 'EMPTY_HTML'
};
}
/* Removing whitespace from between the tags */
HTML = HTML.trim().replace(/>\s+</g, '><');
let response = this._prepareVDOM(HTML);
/* if html is valid only then set it to current html */
if (!response.status) {
return {
'status': 'failed',
'message': 'invalid html sent',
errors: response.errors,
'error': 'INVALID_HTML'
}
}
/* setting valid HTML to current HTML */
this._HTML = HTML;
this._current_VDOM = response.VDOM;
/* convert HTML to vdom */
let stylusOBJ = this._prepareStylusObject(this._current_VDOM);
/* setting current stylusOBJ */
this._current_SylusOBJ = stylusOBJ;
/* increment current StylusOBJ version and push to localstorage */
this._current_StylusOBJ_version++;
let now = new Date().getTime();
let hash = md5(JSON.stringify(this._current_SylusOBJ));
this._stylusOBJContainer[this._current_StylusOBJ_version] = {
version: this._current_StylusOBJ_version,
stylusOBJ: Object.assign({}, this._current_SylusOBJ),
timestamp: now,
md5: hash
};
let result = this._persistStylusIndex();
if (result.status !== 'success') {
/* notify error */
return {
'status': 'failed',
'message': 'unable to save in localstorage',
'error': 'LOCALSTORAGE_FAILED'
}
console.error('unable to save current version');
} else {
/* upload changes to server in case of socket available and reconciliation is enabled */
if (!(('NO_RECONCILE' in this._modifiers) && this._modifiers[
'NO_RECONCILE'] === true)) {
this._facade({
eventName: 'pushToSocket',
arguments: {
'event': 'upload',
'eventPayload': {
requestId: this._current_StylusOBJ_version,
payload: {
bookId: this._bookId,
version: this._current_StylusOBJ_version,
md5: hash,
timestamp: now,
stylusOBJ: Object.assign({}, this._current_SylusOBJ)
}
}
}
});
}
return {
'status': 'success',
'message': 'saved successfully'
}
}
}
/**
* Generate html from StylusOBJ
*/
getHTML() {
if (!this._loaded) {
return {
'status': 'false',
'message': 'SSDK has not finished loading data ....',
'error': 'SSDK_NOT_READY'
};
}
/* return null if current stylus object is not defined or is malformed */
if (this._current_SylusOBJ == null || (!(this._current_SylusOBJ instanceof Object))) {
return {
'status': 'error',
'message': 'Stylus object is not defined',
'error': 'UNDEFINED_STYLUS_OBJ'
};
}
/* TODO: add validation for current Stylus OBJ formed */
if (!this._validateStylusOBJ(this._current_SylusOBJ).status) {
return {
'status': 'error',
'message': 'Stylus object is corrupted',
'error': 'CORRUPTED_STYLUS_OBJ'
};
}
let StylusOBJHTML = this._convertStylusOBJToHTML(this._current_SylusOBJ);
return {
status: 'success',
data: StylusOBJHTML
};
}
/**
* Check manually if SSDK is ready
*/
isReady() {
return this._loaded === true;
}
/* attaches callback to promise to report status of loading */
ready(callback = null) {
/* attach callback to promise */
if (callback) {
this._promise.then(callback, callback);
}
}
}
module.exports = StylusSDK;
/**
* Object related utils to be used project wide
*/
/**
* Checks if specified object has nested attribute
* @param Object obj Object to be tested for presence of attribute
* @param String path Nested attribute selector in dot notation
* @return Boolean Returns true if object has this nested attribute
*/
function hasAttr(obj, path='' /*, 'level1.level2 ... levelN' */) {
if (path == '') {
return false;
}
let selectors = path.split('.');
for (let selector of selectors) {
if (!obj || !obj.hasOwnProperty(selector)) {
return false;
}
obj = obj[selector];
}
return true;
}
/**
* Gets value of nessted attribute if it exists
* @param Object obj Object to be tested for presence of attribute
* @param String path Nested attribute selector in dot notation
* @return Boolean Returns true if object has this nested attribute
*/
function getAttr(obj, path='' /*, 'level1.level2 ... levelN' */) {
if (path == '') {
return false;
}
let selectors = path.split('.');
for (let selector of selectors) {
if (!obj || !obj.hasOwnProperty(selector)) {
return undefined;
}
obj = obj[selector];
}
return obj;
}
/**
* Sets deeply nested attribute of the specified object
* @param Object obj Target object
* @param String path deeply nested path of attr in DOT notation
* @param Mixed value Value to be set to the path in object
*/
function setAttr(obj, path, value) {
let schema = obj; // a moving reference to internal objects within obj
let pList = path.split('.');
for(let path of pList.slice(0,-1)) {
if( !schema[path] ) schema[path] = {}
schema = schema[path];
}
schema[pList[pList.length-1]] = value;
}
/**
* Copies properties from source object to target object based on mapperRegistry object
* mapperRegistry is a container of mapper objects
* mapper object contains from and to keys specfying source anc location paths to be transfered
*
* @param Object source Object from which propties will be copied
* @param Object target Object to which properties will be copied
* @param Object mapperRegistry container of mapper objects
*/
function copyProps(source, target, mapperRegistry) {
mapperRegistry.forEach(mapping => {
if (mapping.from in source) {
if (typeof source[mapping.from] === 'object') {
setAttr(target, mapping.to, Object.assign({}, source[mapping.from]));
}
if ( ['number', 'string', 'boolean'].includes(typeof source[mapping.from]) ) {
setAttr(target, mapping.to, source[mapping.from]);
}
}
});
return Object.assign({}, source);
}
export {hasAttr, copyProps, getAttr}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment