UNPKG

manyfest

Version:

JSON Object Manifest for Data Description and Parsing

933 lines (872 loc) 135 kB
"use strict"; function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } (function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f(); } else if (typeof define === "function" && define.amd) { define([], f); } else { var g; if (typeof window !== "undefined") { g = window; } else if (typeof global !== "undefined") { g = global; } else if (typeof self !== "undefined") { g = self; } else { g = this; } g.Manyfest = f(); } })(function () { var define, module, exports; return function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a; } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r); }, p, p.exports, r, e, n, t); } return n[i].exports; } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]); return o; } return r; }()({ 1: [function (require, module, exports) { /** * Fable Service Base * @author <steven@velozo.com> */ class FableServiceProviderBase { // The constructor can be used in two ways: // 1) With a fable, options object and service hash (the options object and service hash are optional) // 2) With an object or nothing as the first parameter, where it will be treated as the options object constructor(pFable, pOptions, pServiceHash) { // Check if a fable was passed in; connect it if so if (typeof pFable === 'object' && pFable.isFable) { this.connectFable(pFable); } else { this.fable = false; } // initialize options and UUID based on whether the fable was passed in or not. if (this.fable) { this.UUID = pFable.getUUID(); this.options = typeof pOptions === 'object' ? pOptions : {}; } else { // With no fable, check to see if there was an object passed into either of the first two // Parameters, and if so, treat it as the options object this.options = typeof pFable === 'object' && !pFable.isFable ? pFable : typeof pOptions === 'object' ? pOptions : {}; this.UUID = "CORE-SVC-".concat(Math.floor(Math.random() * (99999 - 10000) + 10000)); } // It's expected that the deriving class will set this this.serviceType = "Unknown-".concat(this.UUID); // The service hash is used to identify the specific instantiation of the service in the services map this.Hash = typeof pServiceHash === 'string' ? pServiceHash : !this.fable && typeof pOptions === 'string' ? pOptions : "".concat(this.UUID); } connectFable(pFable) { if (typeof pFable !== 'object' || !pFable.isFable) { let tmpErrorMessage = "Fable Service Provider Base: Cannot connect to Fable, invalid Fable object passed in. The pFable parameter was a [".concat(typeof pFable, "].}"); console.log(tmpErrorMessage); return new Error(tmpErrorMessage); } if (!this.fable) { this.fable = pFable; } if (!this.log) { this.log = this.fable.Logging; } if (!this.services) { this.services = this.fable.services; } if (!this.servicesMap) { this.servicesMap = this.fable.servicesMap; } return true; } } _defineProperty(FableServiceProviderBase, "isFableService", true); module.exports = FableServiceProviderBase; // This is left here in case we want to go back to having different code/base class for "core" services module.exports.CoreServiceProviderBase = FableServiceProviderBase; }, {}], 2: [function (require, module, exports) { // When a boxed property is passed in, it should have quotes of some // kind around it. // // For instance: // MyValues['Name'] // MyValues["Age"] // MyValues[`Cost`] // // This function removes the wrapping quotes. // // Please note it *DOES NOT PARSE* template literals, so backticks just // end up doing the same thing as other quote types. // // TODO: Should template literals be processed? If so what state do they have access to? That should happen here if so. // TODO: Make a simple class include library with these const cleanWrapCharacters = (pCharacter, pString) => { if (pString.startsWith(pCharacter) && pString.endsWith(pCharacter)) { return pString.substring(1, pString.length - 1); } else { return pString; } }; module.exports = cleanWrapCharacters; }, {}], 3: [function (require, module, exports) { /** * @author <steven@velozo.com> */ let libSimpleLog = require('./Manyfest-LogToConsole.js'); /** * Hash Translation * * This is a very simple translation table for hashes, which allows the same schema to resolve * differently based on a loaded translation table. * * This is to prevent the requirement for mutating schemas over and over again when we want to * reuse the structure but look up data elements by different addresses. * * One side-effect of this is that a translation table can "override" the built-in hashes, since * this is always used to resolve hashes before any of the functionCallByHash(pHash, ...) perform * their lookups by hash. * * @class ManyfestHashTranslation */ class ManyfestHashTranslation { constructor(pInfoLog, pErrorLog) { // Wire in logging this.logInfo = typeof pInfoLog === 'function' ? pInfoLog : libSimpleLog; this.logError = typeof pErrorLog === 'function' ? pErrorLog : libSimpleLog; this.translationTable = {}; } translationCount() { return Object.keys(this.translationTable).length; } addTranslation(pTranslation) { // This adds a translation in the form of: // { "SourceHash": "DestinationHash", "SecondSourceHash":"SecondDestinationHash" } if (typeof pTranslation != 'object') { this.logError("Hash translation addTranslation expected a translation be type object but was passed in ".concat(typeof pTranslation)); return false; } let tmpTranslationSources = Object.keys(pTranslation); tmpTranslationSources.forEach(pTranslationSource => { if (typeof pTranslation[pTranslationSource] != 'string') { this.logError("Hash translation addTranslation expected a translation destination hash for [".concat(pTranslationSource, "] to be a string but the referrant was a ").concat(typeof pTranslation[pTranslationSource])); } else { this.translationTable[pTranslationSource] = pTranslation[pTranslationSource]; } }); } removeTranslationHash(pTranslationHash) { if (pTranslationHash in this.translationTable) { delete this.translationTable[pTranslationHash]; } } // This removes translations. // If passed a string, just removes the single one. // If passed an object, it does all the source keys. removeTranslation(pTranslation) { if (typeof pTranslation == 'string') { this.removeTranslationHash(pTranslation); return true; } else if (typeof pTranslation == 'object') { let tmpTranslationSources = Object.keys(pTranslation); tmpTranslationSources.forEach(pTranslationSource => { this.removeTranslation(pTranslationSource); }); return true; } else { this.logError("Hash translation removeTranslation expected either a string or an object but the passed-in translation was type ".concat(typeof pTranslation)); return false; } } clearTranslations() { this.translationTable = {}; } translate(pTranslation) { if (pTranslation in this.translationTable) { return this.translationTable[pTranslation]; } else { return pTranslation; } } } module.exports = ManyfestHashTranslation; }, { "./Manyfest-LogToConsole.js": 4 }], 4: [function (require, module, exports) { /** * @author <steven@velozo.com> */ /** * Manyfest simple logging shim (for browser and dependency-free running) */ const logToConsole = (pLogLine, pLogObject) => { let tmpLogLine = typeof pLogLine === 'string' ? pLogLine : ''; console.log("[Manyfest] ".concat(tmpLogLine)); if (pLogObject) console.log(JSON.stringify(pLogObject)); }; module.exports = logToConsole; }, {}], 5: [function (require, module, exports) { /** * @author <steven@velozo.com> */ const libSimpleLog = require('./Manyfest-LogToConsole.js'); // This is for resolving functions mid-address const libGetObjectValue = require('./Manyfest-ObjectAddress-GetValue.js'); // TODO: Just until this is a fable service. let _MockFable = { DataFormat: require('./Manyfest-ObjectAddress-Parser.js') }; /** * Object Address Resolver * * 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 ManyfestObjectAddressResolverCheckAddressExists */ class ManyfestObjectAddressResolverCheckAddressExists { constructor() { this.getObjectValueClass = new libGetObjectValue(libSimpleLog, libSimpleLog); } // Check if an address exists. // // This is necessary because the getValueAtAddress function is ambiguous on // whether the element/property is actually there or not (it returns // undefined whether the property exists or not). This function checks for // existance and returns true or false dependent. checkAddressExists(pObject, pAddress, pRootObject) { // TODO: Should these throw an error? // Make sure pObject is an object if (typeof pObject != 'object') return false; // Make sure pAddress is a string if (typeof pAddress != 'string') return false; // Set the root object to the passed-in object if it isn't set yet. This is expected to be the root object. // NOTE: This was added to support functions mid-stream let tmpRootObject = typeof pRootObject == 'undefined' ? pObject : pRootObject; // DONE: Make this work for things like SomeRootObject.Metadata["Some.People.Use.Bad.Object.Property.Names"] let tmpAddressPartBeginning = _MockFable.DataFormat.stringGetFirstSegment(pAddress); // This is the terminal address string (no more dots so the RECUSION ENDS IN HERE somehow) if (tmpAddressPartBeginning.length == pAddress.length) { // Check if the address refers to a boxed property let tmpBracketStartIndex = pAddress.indexOf('['); let tmpBracketStopIndex = pAddress.indexOf(']'); // Check if there is a function somewhere in the address... parenthesis start should only be in a function let tmpFunctionStartIndex = pAddress.indexOf('('); // NOTE THAT FUNCTIONS MUST RESOLVE FIRST // Functions look like this // MyFunction() // MyFunction(Some.Address) // MyFunction(Some.Address,Some.Other.Address) // MyFunction(Some.Address,Some.Other.Address,Some.Third.Address) // // This could be enhanced to allow purely numeric and string values to be passed to the function. For now, // To heck with that. This is a simple function call. // // The requirements to detect a function are: // 1) The start bracket is after character 0 if (tmpFunctionStartIndex > 0 // 2) The end bracket is after the start bracket && _MockFable.DataFormat.stringCountEnclosures(pAddress) > 0) { let tmpFunctionAddress = pAddress.substring(0, tmpFunctionStartIndex).trim(); if (tmpFunctionAddress in pObject && typeof pObject[tmpFunctionAddress] == 'function') { return true; } else { // The address suggests it is a function, but it is not. return false; } } // 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 else 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(); // 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; } // 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); // 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); // Check if the property exists. return tmpBoxedPropertyReference in pObject[tmpBoxedPropertyName]; } else { // Use the new in operator to see if the element is in the array return tmpBoxedPropertyNumber in pObject[tmpBoxedPropertyName]; } } else { // Check if the property exists return pAddress in pObject; } } else { let tmpSubObjectName = tmpAddressPartBeginning; let tmpNewAddress = pAddress.substring(tmpAddressPartBeginning.length + 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(']'); // Check if there is a function somewhere in the address... parenthesis start should only be in a function let tmpFunctionStartIndex = tmpSubObjectName.indexOf('('); // NOTE THAT FUNCTIONS MUST RESOLVE FIRST // Functions look like this // MyFunction() // MyFunction(Some.Address) // MyFunction(Some.Address,Some.Other.Address) // MyFunction(Some.Address,Some.Other.Address,Some.Third.Address) // // This could be enhanced to allow purely numeric and string values to be passed to the function. For now, // To heck with that. This is a simple function call. // // The requirements to detect a function are: // 1) The start bracket is after character 0 if (tmpFunctionStartIndex > 0 // 2) The end bracket is after the start bracket && _MockFable.DataFormat.stringCountEnclosures(tmpSubObjectName) > 0) { let tmpFunctionAddress = tmpSubObjectName.substring(0, tmpFunctionStartIndex).trim(); //tmpParentAddress = `${tmpParentAddress}${(tmpParentAddress.length > 0) ? '.' : ''}${tmpSubObjectName}`; if (!typeof pObject[tmpFunctionAddress] == 'function') { // The address suggests it is a function, but it is not. return false; } // Now see if the function has arguments. // Implementation notes: * ARGUMENTS MUST SHARE THE SAME ROOT OBJECT CONTEXT * let tmpFunctionArguments = _MockFable.DataFormat.stringGetSegments(_MockFable.DataFormat.stringGetEnclosureValueByIndex(tmpSubObjectName.substring(tmpFunctionAddress.length), 0), ','); if (tmpFunctionArguments.length == 0 || tmpFunctionArguments[0] == '') { // No arguments... just call the function (bound to the scope of the object it is contained withing) if (tmpFunctionAddress in pObject) { try { return this.checkAddressExists(pObject[tmpFunctionAddress].apply(pObject), tmpNewAddress, tmpRootObject); } catch (pError) { // The function call failed, so the address doesn't exist libSimpleLog.log("Error calling function ".concat(tmpFunctionAddress, " (address [").concat(pAddress, "]): ").concat(pError.message)); return false; } } else { // The function doesn't exist, so the address doesn't exist libSimpleLog.log("Function ".concat(tmpFunctionAddress, " does not exist (address [").concat(pAddress, "])")); return false; } } else { let tmpArgumentValues = []; let tmpRootObject = typeof pRootObject == 'undefined' ? pObject : pRootObject; // Now get the value for each argument for (let i = 0; i < tmpFunctionArguments.length; i++) { // Resolve the values for each subsequent entry // NOTE: This is where the resolves get really tricky. Recursion within recursion. Programming gom jabbar, yo. tmpArgumentValues.push(this.getObjectValueClass.getValueAtAddress(tmpRootObject, tmpFunctionArguments[i])); } //return this.checkAddressExists(pObject[tmpFunctionAddress].apply(pObject, tmpArgumentValues), tmpNewAddress, tmpRootObject); if (tmpFunctionAddress in pObject) { try { return this.checkAddressExists(pObject[tmpFunctionAddress].apply(pObject, tmpArgumentValues), tmpNewAddress, tmpRootObject); } catch (pError) { // The function call failed, so the address doesn't exist libSimpleLog.log("Error calling function ".concat(tmpFunctionAddress, " (address [").concat(pAddress, "]): ").concat(pError.message)); return false; } } else { // The function doesn't exist, so the address doesn't exist libSimpleLog.log("Function ".concat(tmpFunctionAddress, " does not exist (address [").concat(pAddress, "])")); return false; } } } // 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 else 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); // 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]) == isNaN(tmpBoxedPropertyNumber)) { // Because this is an impossible address, the property doesn't exist // TODO: Should we throw an error in this condition? 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); // Recurse directly into the subobject return this.checkAddressExists(pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference], tmpNewAddress, tmpRootObject); } else { // We parsed a valid number out of the boxed property name, so recurse into the array return this.checkAddressExists(pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber], tmpNewAddress, tmpRootObject); } } // 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') { return false; } else if (tmpSubObjectName in pObject) { // If there is already a subobject pass that to the recursive thingy return this.checkAddressExists(pObject[tmpSubObjectName], tmpNewAddress, tmpRootObject); } else { // Create a subobject and then pass that pObject[tmpSubObjectName] = {}; return this.checkAddressExists(pObject[tmpSubObjectName], tmpNewAddress, tmpRootObject); } } } } ; module.exports = ManyfestObjectAddressResolverCheckAddressExists; }, { "./Manyfest-LogToConsole.js": 4, "./Manyfest-ObjectAddress-GetValue.js": 7, "./Manyfest-ObjectAddress-Parser.js": 8 }], 6: [function (require, module, exports) { /** * @author <steven@velozo.com> */ let libSimpleLog = require('./Manyfest-LogToConsole.js'); let fCleanWrapCharacters = require('./Manyfest-CleanWrapCharacters.js'); let fParseConditionals = require("../source/Manyfest-ParseConditionals.js"); /** * Object Address Resolver - DeleteValue * * 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. * * TODO: Once we validate this pattern is good to go, break these out into * three separate modules. * * @class ManyfestObjectAddressResolverDeleteValue */ class ManyfestObjectAddressResolverDeleteValue { constructor(pInfoLog, pErrorLog) { // Wire in logging this.logInfo = typeof pInfoLog == 'function' ? pInfoLog : libSimpleLog; this.logError = typeof pErrorLog == 'function' ? pErrorLog : libSimpleLog; this.cleanWrapCharacters = fCleanWrapCharacters; } // TODO: Dry me checkRecordFilters(pAddress, pRecord) { return fParseConditionals(this, pAddress, pRecord); } // Delete the value of an element at an address deleteValueAtAddress(pObject, pAddress, pParentAddress) { // Make sure pObject (the object we are meant to be recursing) is an object (which could be an array or object) if (typeof pObject != 'object') return undefined; // Make sure pAddress (the address we are resolving) is a string if (typeof pAddress != 'string') return undefined; // Stash the parent address for later resolution let tmpParentAddress = ""; if (typeof pParentAddress == 'string') { tmpParentAddress = pParentAddress; } // TODO: Make this work for things like SomeRootObject.Metadata["Some.People.Use.Bad.Object.Property.Names"] let tmpSeparatorIndex = pAddress.indexOf('.'); // This is the terminal address string (no more dots so the RECUSION ENDS IN HERE somehow) if (tmpSeparatorIndex == -1) { // Check if the address refers to a boxed property let tmpBracketStartIndex = pAddress.indexOf('['); let tmpBracketStopIndex = pAddress.indexOf(']'); // Check for the Object Set Type marker. // Note this will not work with a bracket in the same address box set let tmpObjectTypeMarkerIndex = 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(); // 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; } // 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); // 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); // Return the value in the property delete pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference]; return true; } else { delete pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber]; return true; } } // The requirements to detect a boxed set element are: // 1) The start bracket is after character 0 else if (tmpBracketStartIndex > 0 // 2) The end bracket is after the start bracket && tmpBracketStopIndex > tmpBracketStartIndex // 3) There is nothing in the brackets && tmpBracketStopIndex - tmpBracketStartIndex == 1) { let tmpBoxedPropertyName = pAddress.substring(0, tmpBracketStartIndex).trim(); if (!Array.isArray(pObject[tmpBoxedPropertyName])) { // We asked for a set from an array but it isnt' an array. return false; } let tmpInputArray = pObject[tmpBoxedPropertyName]; // Count from the end to the beginning so splice doesn't %&%#$ up the array for (let i = tmpInputArray.length - 1; i >= 0; i--) { // The filtering is complex but allows config-based metaprogramming directly from schema let tmpKeepRecord = this.checkRecordFilters(pAddress, tmpInputArray[i]); if (tmpKeepRecord) { // Delete elements end to beginning tmpInputArray.splice(i, 1); } } return true; } // The object has been flagged as an object set, so treat it as such else if (tmpObjectTypeMarkerIndex > 0) { let tmpObjectPropertyName = pAddress.substring(0, tmpObjectTypeMarkerIndex).trim(); if (typeof pObject[tmpObjectPropertyName] != 'object') { // We asked for a set from an array but it isnt' an array. return false; } delete pObject[tmpObjectPropertyName]; return true; } else { // Now is the point in recursion to return the value in the address delete pObject[pAddress]; return true; } } else { let tmpSubObjectName = pAddress.substring(0, tmpSeparatorIndex); let tmpNewAddress = pAddress.substring(tmpSeparatorIndex + 1); // BOXED ELEMENTS // 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); // 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]) == isNaN(tmpBoxedPropertyNumber)) { return false; } // Check if the boxed property is an object. if (typeof pObject[tmpBoxedPropertyName] != 'object') { 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); // Continue to manage the parent address for recursion tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpSubObjectName); // Recurse directly into the subobject return this.deleteValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyReference], tmpNewAddress, tmpParentAddress); } else { // Continue to manage the parent address for recursion tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpSubObjectName); // We parsed a valid number out of the boxed property name, so recurse into the array return this.deleteValueAtAddress(pObject[tmpBoxedPropertyName][tmpBoxedPropertyNumber], tmpNewAddress, tmpParentAddress); } } // The requirements to detect a boxed set element are: // 1) The start bracket is after character 0 else if (tmpBracketStartIndex > 0 // 2) The end bracket is after the start bracket && tmpBracketStopIndex > tmpBracketStartIndex // 3) There is nothing in the brackets && tmpBracketStopIndex - tmpBracketStartIndex == 1) { let tmpBoxedPropertyName = pAddress.substring(0, tmpBracketStartIndex).trim(); if (!Array.isArray(pObject[tmpBoxedPropertyName])) { // We asked for a set from an array but it isnt' an array. return false; } // We need to enumerate the array and grab the addresses from there. let tmpArrayProperty = pObject[tmpBoxedPropertyName]; // Managing the parent address is a bit more complex here -- the box will be added for each element. tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpBoxedPropertyName); // The container object is where we have the "Address":SOMEVALUE pairs let tmpContainerObject = {}; for (let i = 0; i < tmpArrayProperty.length; i++) { let tmpPropertyParentAddress = "".concat(tmpParentAddress, "[").concat(i, "]"); let tmpValue = this.deleteValueAtAddress(pObject[tmpBoxedPropertyName][i], tmpNewAddress, tmpPropertyParentAddress); tmpContainerObject["".concat(tmpPropertyParentAddress, ".").concat(tmpNewAddress)] = tmpValue; } return tmpContainerObject; } // OBJECT SET // Note this will not work with a bracket in the same address box set let tmpObjectTypeMarkerIndex = pAddress.indexOf('{}'); if (tmpObjectTypeMarkerIndex > 0) { let tmpObjectPropertyName = pAddress.substring(0, tmpObjectTypeMarkerIndex).trim(); if (typeof pObject[tmpObjectPropertyName] != 'object') { // We asked for a set from an array but it isnt' an array. return false; } // We need to enumerate the Object and grab the addresses from there. let tmpObjectProperty = pObject[tmpObjectPropertyName]; let tmpObjectPropertyKeys = Object.keys(tmpObjectProperty); // Managing the parent address is a bit more complex here -- the box will be added for each element. tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpObjectPropertyName); // The container object is where we have the "Address":SOMEVALUE pairs let tmpContainerObject = {}; for (let i = 0; i < tmpObjectPropertyKeys.length; i++) { let tmpPropertyParentAddress = "".concat(tmpParentAddress, ".").concat(tmpObjectPropertyKeys[i]); let tmpValue = this.deleteValueAtAddress(pObject[tmpObjectPropertyName][tmpObjectPropertyKeys[i]], tmpNewAddress, tmpPropertyParentAddress); // The filtering is complex but allows config-based metaprogramming directly from schema let tmpKeepRecord = this.checkRecordFilters(pAddress, tmpValue); if (tmpKeepRecord) { tmpContainerObject["".concat(tmpPropertyParentAddress, ".").concat(tmpNewAddress)] = tmpValue; } } return tmpContainerObject; } // 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') { return undefined; } else if (tmpSubObjectName in pObject) { // If there is already a subobject pass that to the recursive thingy // Continue to manage the parent address for recursion tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpSubObjectName); return this.deleteValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, tmpParentAddress); } else { // Create a subobject and then pass that // Continue to manage the parent address for recursion tmpParentAddress = "".concat(tmpParentAddress).concat(tmpParentAddress.length > 0 ? '.' : '').concat(tmpSubObjectName); pObject[tmpSubObjectName] = {}; return this.deleteValueAtAddress(pObject[tmpSubObjectName], tmpNewAddress, tmpParentAddress); } } } } ; module.exports = ManyfestObjectAddressResolverDeleteValue; }, { "../source/Manyfest-ParseConditionals.js": 11, "./Manyfest-CleanWrapCharacters.js": 2, "./Manyfest-LogToConsole.js": 4 }], 7: [function (require, module, exports) { /** * @author <steven@velozo.com> */ let libSimpleLog = require('./Manyfest-LogToConsole.js'); let fCleanWrapCharacters = require('./Manyfest-CleanWrapCharacters.js'); let fParseConditionals = require("../source/Manyfest-ParseConditionals.js"); let _MockFable = { DataFormat: require('./Manyfest-ObjectAddress-Parser.js') }; /** * Object Address Resolver - GetValue * * 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. * * TODO: Once we validate this pattern is good to go, break these out into * three separate modules. * * @class ManyfestObjectAddressResolverGetValue */ class ManyfestObjectAddressResolverGetValue { constructor(pInfoLog, pErrorLog) { // Wire in logging this.logInfo = typeof pInfoLog == 'function' ? pInfoLog : libSimpleLog; this.logError = typeof pErrorLog == 'function' ? pErrorLog : libSimpleLog; this.cleanWrapCharacters = fCleanWrapCharacters; } checkRecordFilters(pAddress, pRecord) { return fParseConditionals(this, pAddress, pRecord); } // Get the value of an element at an address getValueAtAddress(pObject, pAddress, pParentAddress, pRootObject) { // Make sure pObject (the object we are meant to be recursing) is an object (which could be an array or object) if (typeof pObject != 'object') return undefined; // Make sure pAddress (the address we are resolving) is a string if (typeof pAddress != 'string') return undefined; // Stash the parent address for later resolution let tmpParentAddress = ""; if (typeof pParentAddress == 'string') { tmpParentAddress = pParentAddress; } // Set the root object to the passed-in object if it isn't set yet. This is expected to be the root object. let tmpRootObject = typeof pRootObject == 'undefined' ? pObject : pRootObject; // DONE: Make this work for things like SomeRootObject.Metadata["Some.People.Use.Bad.Object.Property.Names"] let tmpAddressPartBeginning = _MockFable.DataFormat.stringGetFirstSegment(pAddress); // Adding simple back-navigation in objects if (tmpAddressPartBeginning == '') { // Given an address of "Bundle.Contract.IDContract...Project.IDProject" the ... would be interpreted as two back-navigations from IDContract. // When the address is passed in, though, the first . is already eliminated. So we can count the dots. let tmpParentAddressParts = _MockFable.DataFormat.stringGetSegments(tmpParentAddress); let tmpBackNavigationCount = 0; // Count the number of dots for (let i = 0; i < pAddress.length; i++) { if (pAddress.charAt(i) != '.') { break; } tmpBackNavigationCount++; } let tmpParentAddressLength = tmpParentAddressParts.length - tmpBackNavigationCount; if (tmpParentAddressLength < 0) { // We are trying to back navigate more than we can. // TODO: Should this be undefined or should we bank out at the bottom and try to go forward? // This seems safest for now. return undefined; } else { // We are trying to back navigate to a parent object.