manyfest
Version:
JSON Object Manifest for Data Description and Parsing
264 lines (240 loc) • 10.5 kB
JavaScript
/**
* @author <steven@velozo.com>
*/
let libSimpleLog = require('./Manyfest-LogToConsole.js');
let fCleanWrapCharacters = require('./Manyfest-CleanWrapCharacters.js');
/**
* Object Address Resolver - SetValue
*
* IMPORTANT NOTE: This code is intentionally more verbose than necessary, to
* be extremely clear what is going on in the recursion for
* each of the three address resolution functions.
*
* Although there is some opportunity to repeat ourselves a
* bit less in this codebase (e.g. with detection of arrays
* versus objects versus direct properties), it can make
* debugging.. challenging. The minified version of the code
* optimizes out almost anything repeated in here. So please
* be kind and rewind... meaning please keep the codebase less
* terse and more verbose so humans can comprehend it.
*
*
* @class ManyfestObjectAddressSetValue
*/
class ManyfestObjectAddressSetValue
{
constructor(pInfoLog, pErrorLog)
{
// Wire in logging
this.logInfo = (typeof(pInfoLog) == 'function') ? pInfoLog : libSimpleLog;
this.logError = (typeof(pErrorLog) == 'function') ? pErrorLog : libSimpleLog;
this.cleanWrapCharacters = fCleanWrapCharacters;
}
// Set the value of an element at an address
setValueAtAddress (pObject, pAddress, pValue)
{
// Make sure pObject is an object
if (typeof(pObject) != 'object') return false;
// Make sure pAddress is a string
if (typeof(pAddress) != 'string') return false;
let tmpSeparatorIndex = pAddress.indexOf('.');
if (tmpSeparatorIndex == -1)
{
// Check if it's a boxed property
let tmpBracketStartIndex = pAddress.indexOf('[');
let tmpBracketStopIndex = pAddress.indexOf(']');
// Boxed elements look like this:
// MyValues[10]
// MyValues['Name']
// MyValues["Age"]
// MyValues[`Cost`]
//
// When we are passed SomeObject["Name"] this code below recurses as if it were SomeObject.Name
// The requirements to detect a boxed element are:
// 1) The start bracket is after character 0
if ((tmpBracketStartIndex > 0)
// 2) The end bracket has something between them
&& (tmpBracketStopIndex > tmpBracketStartIndex)
// 3) There is data
&& (tmpBracketStopIndex - tmpBracketStartIndex > 1))
{
// The "Name" of the Object contained too the left of the bracket
let tmpBoxedPropertyName = pAddress.substring(0, tmpBracketStartIndex).trim();
// The "Reference" to the property within it, either an array element or object property
let tmpBoxedPropertyReference = pAddress.substring(tmpBracketStartIndex+1, tmpBracketStopIndex).trim();
// Attempt to parse the reference as a number, which will be used as an array element
let tmpBoxedPropertyNumber = parseInt(tmpBoxedPropertyReference, 10);
let tmpIndexIsNumeric = !isNaN(tmpBoxedPropertyNumber);
if (pObject[tmpBoxedPropertyName] == null)
{
if (tmpIndexIsNumeric)
{
pObject[tmpBoxedPropertyName] = [];
}
else
{
pObject[tmpBoxedPropertyName] = {};
}
}
// If the subproperty doesn't test as a proper Object, none of the rest of this is possible.
// This is a rare case where Arrays testing as Objects is useful
if (typeof(pObject[tmpBoxedPropertyName]) !== 'object')
{
return false;
}
// Guard: If the referrant is a number and the boxed property is not an array, or vice versa, return undefined.
// This seems confusing to me at first read, so explaination:
// Is the Boxed Object an Array? TRUE
// And is the Reference inside the boxed Object not a number? TRUE
// --> So when these are in agreement, it's an impossible access state
if (Array.isArray(pObject[tmpBoxedPropertyName]) == isNaN(tmpBoxedPropertyNumber))
{
return false;
}
// 4) If the middle part is *only* a number (no single, double or backtick quotes) it is an array element,
// otherwise we will try to treat it as a dynamic object property.
if (isNaN(tmpBoxedPropertyNumber))
{
// This isn't a number ... let's treat it as a dynamic object property.
// We would expect the property to be wrapped in some kind of quotes so strip them
tmpBoxedPropertyReference = this.cleanWrapCharacters('"', tmpBoxedPropertyReference);
tmpBoxedPropertyReference = this.cleanWrapCharacters('`', tmpBoxedPropertyReference);
tmpBoxedPropertyReference = this.cleanWrapCharacters("'", tmpBoxedPropertyReference);
if (!(tmpBoxedPropertyReference in pObject[tmpBoxedPropertyName]))
{
// If the subobject doesn't exist, create it
pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference] = {};
}
// Return the value in the property
//TODO: For cases where we have chained [][] properties, this needs to recurse somehow
pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference] = pValue;
return true;
}
else
{
while(pObject[tmpBoxedPropertyName].length < (tmpBoxedPropertyNumber + 1))
{
// If the subobject doesn't exist, create it
pObject[tmpBoxedPropertyName].push({});
}
pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber] = pValue;
return true;
}
}
else
{
// Now is the time in recursion to set the value in the object
pObject[pAddress] = pValue;
return true;
}
}
else
{
let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex);
let tmpNewAddress = pAddress.substring(tmpSeparatorIndex+1);
// Test if the tmpNewAddress is an array or object
// Check if it's a boxed property
let tmpBracketStartIndex = tmpSubObjectName.indexOf('[');
let tmpBracketStopIndex = tmpSubObjectName.indexOf(']');
// Boxed elements look like this:
// MyValues[42]
// MyValues['Color']
// MyValues["Weight"]
// MyValues[`Diameter`]
//
// When we are passed SomeObject["Name"] this code below recurses as if it were SomeObject.Name
// The requirements to detect a boxed element are:
// 1) The start bracket is after character 0
if ((tmpBracketStartIndex > 0)
// 2) The end bracket has something between them
&& (tmpBracketStopIndex > tmpBracketStartIndex)
// 3) There is data
&& (tmpBracketStopIndex - tmpBracketStartIndex > 1))
{
let tmpBoxedPropertyName = tmpSubObjectName.substring(0, tmpBracketStartIndex).trim();
let tmpBoxedPropertyReference = tmpSubObjectName.substring(tmpBracketStartIndex+1, tmpBracketStopIndex).trim();
let tmpBoxedPropertyNumber = parseInt(tmpBoxedPropertyReference, 10);
let tmpIndexIsNumeric = !isNaN(tmpBoxedPropertyNumber);
//if (typeof(pObject[tmpBoxedPropertyName]) !== 'object')
if (pObject[tmpBoxedPropertyName] == null)
{
if (tmpIndexIsNumeric)
{
pObject[tmpBoxedPropertyName] = [];
}
else
{
pObject[tmpBoxedPropertyName] = {};
}
}
// Guard: If the referrant is a number and the boxed property is not an array, or vice versa, return undefined.
// This seems confusing to me at first read, so explaination:
// Is the Boxed Object an Array? TRUE
// And is the Reference inside the boxed Object not a number? TRUE
// --> So when these are in agreement, it's an impossible access state
// This could be a failure in the recursion chain because they passed something like this in:
// StudentData.Sections.Algebra.Students[1].Tardy
// BUT
// StudentData.Sections.Algebra.Students is an object, so the [1].Tardy is not possible to access
// This could be a failure in the recursion chain because they passed something like this in:
// StudentData.Sections.Algebra.Students["JaneDoe"].Grade
// BUT
// StudentData.Sections.Algebra.Students is an array, so the ["JaneDoe"].Grade is not possible to access
// TODO: Should this be an error or something? Should we keep a log of failures like this?
if (Array.isArray(pObject[tmpBoxedPropertyName]) != tmpIndexIsNumeric)
{
return false;
}
//This is a bracketed value
// 4) If the middle part is *only* a number (no single, double or backtick quotes) it is an array element,
// otherwise we will try to reat it as a dynamic object property.
if (isNaN(tmpBoxedPropertyNumber))
{
// This isn't a number ... let's treat it as a dynanmic object property.
tmpBoxedPropertyReference = this.cleanWrapCharacters('"', tmpBoxedPropertyReference);
tmpBoxedPropertyReference = this.cleanWrapCharacters('`', tmpBoxedPropertyReference);
tmpBoxedPropertyReference = this.cleanWrapCharacters("'", tmpBoxedPropertyReference);
if (!(tmpBoxedPropertyReference in pObject[tmpBoxedPropertyName]))
{
// If the subobject doesn't exist, create it
pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference] = {};
}
// Recurse directly into the subobject
return this.setValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference], tmpNewAddress, pValue);
}
else
{
while(pObject[tmpBoxedPropertyName].length < (tmpBoxedPropertyNumber + 1))
{
// If the subobject doesn't exist, create it
pObject[tmpBoxedPropertyName].push({});
}
// We parsed a valid number out of the boxed property name, so recurse into the array
return this.setValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber], tmpNewAddress, pValue);
}
}
// If there is an object property already named for the sub object, but it isn't an object
// then the system can't set the value in there. Error and abort!
if ((tmpSubObjectName in pObject) && typeof(pObject[tmpSubObjectName]) !== 'object')
{
if (!('__ERROR' in pObject))
pObject['__ERROR'] = {};
// Put it in an error object so data isn't lost
pObject['__ERROR'][pAddress] = pValue;
return false;
}
else if (tmpSubObjectName in pObject)
{
// If there is already a subobject pass that to the recursive thingy
return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
}
else
{
// Create a subobject and then pass that
pObject[tmpSubObjectName] = {};
return this.setValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, pValue);
}
}
}
};
module.exports = ManyfestObjectAddressSetValue;