UNPKG

feature-u

Version:

Feature Based Project Organization for React

1,022 lines (855 loc) 43.8 kB
import verify from '../util/verify'; import isString from 'lodash.isstring'; import isPlainObject from 'lodash.isplainobject'; import isFunction from 'lodash.isfunction'; import fassetValidations from './fassetValidations'; import {MyObj} from '../util/mySpace'; import logf from '../util/logf'; /** * An internal creator of the {{book.api.FassetsObject}}. Accumulates * the {{book.api.fassetsAspect}} promoted by the set of supplied * `activeFeatures`. * * @param {Feature[]} activeFeatures the active features from which we * accumulate feature assets through the {{book.api.fassetsAspect$}}. * * @return {Fassets} a new Fassets object (promoted by launchApp()). * * @private * * @function createFassets */ export default function createFassets(activeFeatures) { // PRIVATE: active features (used in hasFeature()) const _hasFeature = { /* dynamically maintained ... SAMPLE: feature1: true, feature2: true, ... */ }; // PRIVATE: fassets resources (with meta data) // - defined via Feature.fassets.define/defineUse const _resources = { /* dynamically maintained ... SAMPLE: '{fassetsKey}': { // ex: 'action.openView' ... NO wildcards allowed val: {whatever}, // resource value definingFeature: {featureName}, // the feature defining this resource defineUse: boolean, // directive used in defining this resource (false: define. true: defineUse) }, ... */ }; // PRIVATE: a string blob of all resolved fassetKeys delimited by new-line // - used by fassets.get() method to fetch all keys matching a regexp // - maintained in feature expansion order // ... the same order fassets.get() exposes multiple entries // - highly optimal technique, where a single regexp search // is used per fassets.get(wildcard) // ... for even better performance, get() also caches it's results!! var _fassetsKeysBlob = ''; // PRIVATE: fassets usage contract (with meta data) // - defined via Feature.fassets.use const _usage = { /* dynamically maintained ... SAMPLE: '{useKey}': { // ex: 'MainPage.*.link' ... wildcards allowed usingWildCard: true/false // does this usage contract employ wildcards? required: true/false, // optionality (required takes precedence for multiple entries) validateFn: func, // validation function (based on registered keywords) definingFeatures: [{featureName}], // what feature(s) defined this "use" contract }, ... */ }; // PRIVATE: cache all fassets.get() searches, providing a significant optimization // - especially in the context of React Component usage, // which repeats frequently within each UI render // - this is feasible because fasset resources are pre-loaded up-front, // and these set of resources will not change const _searchCache = { /* dynamically maintained ... SAMPLE: 'MainPage.*.link': [...results], 'MainPage.*.body': [], // empty array for no results 'selector.currentView': result, 'selector.currentView': UNDEFINED, // special UNDEFINED for no results (to distguish from entry NOT in cache) ... */ }; // PRIVATE: special value used in cache to allow not-found (undefined) entry // to be cached in a recognizable way const UNDEFINED = 'UNDEFINED'; /** * @typedef {Object} Fassets * * The `fassets` object _(emitted from {{book.api.launchApp}})_ is * an accumulation of **public feature assets gathered from all * features**. It facilitates {{book.guide.crossCom}} by promoting * the public resources of any given feature. * * **SideBar**: The term `fassets` is a play on words. While it is * pronounced "facet" _and is loosely related to this term_, it is * spelled fassets (i.e. feature assets). * * There are 3 different ways to reference the resources contained * in the `fassets` object: * * 1. You may directly dereference them. As an example, an * '`action.openView`' resource can be dereferenced as follows: * * ```js * fassets.action.openView('mainView'); * ``` * * 2. You may use the {{book.api.Fassets_get}} method, which * can collect multiple resources (using {{book.guide.crossCom_wildcards}}). * * 3. Your UI components may indirectly access `fassets` resources * through the {{book.api.withFassets}} Higher-order Component * (HoC). * * **SideBar**: There are several ways to get a handle to the * `fassets` object _(see * {{book.guide.crossCom_obtainingFassetsObject}})_. * * For more information, please refer to {{book.guide.crossCom}} and * {{book.guide.crossCom_fassetsBasics}}. */ const _fassets = { // *** // *** normalized fassets accumulation over all features // *** /* dynamically maintained ... SAMPLE: MainPage: { cart: { link: () => <Link to="/cart">Cart</Link>, body: () => <Route path="/cart" component={ShoppingCart}/>, }, search: { link: () => <Link to="/search">Search</Link>, body: () => <Route path="/search" component={Search}/>, }, }, ... */ // *** // *** pre-defined API (methods) // *** /** * Get (i.e. fetch) the resource(s) corresponding to the supplied * `fassetsKey`. * * The `fassets.get()` method is an alternative to directly * dereferencing the `fassets` object ... the advantage being: * * 1. it can accumulate a series of resources (when * {{book.guide.crossCom_wildcards}} are used) * * 2. and it can more gracefully return undefined at any level * within the federated namespace path * * Regarding the `fassetsKey`: * * - It is case-sensitive _(as are the defined resources)_. * * - It may contain {{book.guide.crossCom_wildcards}} (`*`), resulting in a * multiple resources being returned (a resource array), matching the * supplied pattern * * - Matches are restricted to the actual fassetKeys registered through * the {{book.api.fassetsAspect}} `define`/`defineUse` directives. In * other words, the matching algorithm will **not** drill into the * resource itself (assuming it is an object with depth). * * - The special **dot** keyword (`'.'`) will return the fassets * object itself _(in the same tradition as "current directory")_. * * - `'@withKeys'`: * * In some cases, you may wish to know the corresponding * `fassetsKey` of the returned resource. This is especially true * when multiple resources are returned _(using wildcards)_. As * an example, JSX requires unique keys for array injections _(the * `fassetsKey` is a prime candidate for this, since it is * guaranteed to be unique)_. * * To accomplish this, simply suffix the `fassetsKey` with the * keyword: `'@withKeys'`. When this is encountered, the * resource returned is a two-element array: `[fassetsKey, * resource]`. _**SideBar**: We use this suffix technique (as * opposed to an additional parameter) to be consistent with how * {{book.api.withFassets}} operates._ * * **SideBar**: The `fassets.get()` method is the basis of both the * {{book.api.useFassets}} {{book.ext.reactHook}}, and the * {{book.api.withFassets}} Higher-order Component (HoC). * * @param {string} fassetsKey the key of the resource(s) to fetch. * This may include wildcards (`*`), as well as the `'@withKeys'` * suffix _(see discussion above)_. * * @return {resource|resource[]} the requested fassets resource(s). * * - **without wildcards**, a single resource is returned * _(`undefined` for none)_. * * ```js * 'a.b.c': abcResource * 'a.b.c@withKeys': ['a.b.c', abcResource] * ``` * * - **with wildcards**, the return is a resource array, in order * of feature expansion _(empty array for none)_. * * ```js * 'a.*': [ab1Resource, ab2Resource, ...] * 'a.*@withKeys': [ ['a.b1', ab1Resource], ['a.b2', ab2Resource], ... ] * ``` * * @method Fassets.get */ get: (fassetsKey) => { // validate parameters const check = verify.prefix('fassets.get() parameter violation: '); check(fassetsKey, 'fassetsKey is required'); check(isString(fassetsKey), `fassetsKey must be a string ... ${fassetsKey}`); // interpret optional @directive keywords suffixed in fassetsKey const [raw_fassetsKey, ...directives] = fassetsKey.split('@'); check(raw_fassetsKey, `fassetsKey: '${fassetsKey}' cannot contain only directives`); const { withKeys=false } = directives.reduce( (accum, directive) => { if (directive === 'withKeys') { // @withKeys directive accum.withKeys = true; } else { check(false, `fassetsKey: '${fassetsKey}' contains an unrecognized keyword directive: '@${directive}'`); } return accum; }, {}); // use cached value (when available) // ... NOTE: for cache purposes, fassetsKey correctly includes any @directives // (which can alter the result) var result = _searchCache[fassetsKey]; if (result) { return result===UNDEFINED ? undefined : result; } // resolve resource of supplied key const resolveResource = (key) => { const resource = key==='.' ? _fassets // special DOT keyword ... return self (i.e. fassets) : _resources[key] ? _resources[key].val // resource found : undefined; // resource not found return withKeys // interpret @withKeys directive ? [key, resource] // ... [fassetsKey, resource] : resource; // ... resource }; // resolve get() when not seen before if (containsWildCard(raw_fassetsKey)) { // supplied fassetsKey has wildcards ... do regexp search // locate all keys matching the fassetsKey (with wildcards) // ... order is same as feature expansion order // ... empty array for NO match const keys = matchAll(_fassetsKeysBlob, createRegExp(raw_fassetsKey)); // convert keys to actual resource values result = keys.map( key => resolveResource(key) ); } else { // supplied fassetsKey has NO wildcards ... dereference directly // convert dereferenced resource (when found) to actual resource value // ... undefined for NOT found result = resolveResource(raw_fassetsKey); } // maintain cache // ... NOTE: for cache purposes, fassetsKey correctly includes any @directives // (which can alter the result) _searchCache[fassetsKey] = result===undefined ? UNDEFINED : result; // that's all folks return result; }, /** * Return an indicator as to whether the supplied feature is * active or not. * * **Note**: As an alternative to using this method, you can * conditionally reason over the existence of "well-known fasset * resources" specific to a given feature. * * @param {string} featureName the name of the feature to check. * * @return {boolean} **true**: is active, **false**: is not active * (or doesn't exist). * * @method Fassets.hasFeature */ hasFeature: (featureName) => { // validate parameters const check = verify.prefix('fassets.hasFeature() parameter violation: '); check(featureName, 'featureName is required'); check(isString(featureName), 'featureName must be a string'); // return indicator return _hasFeature[featureName] ? true : false; }, }; //*--------------------------------------------------------------------------- // OK: We are now ready to interpret/accumulate all feature fassets!! // - This requires multiple passes through our features. // ... As a simple example: // We cannot validate the usage contracts (found in fassets.use), // until all resources are accumulated (via fassets.define/defineUse) // - The comment blocks (below) breaks the process down in "stages". // ... Think of the mighty Saturn V rocket, which powered the Apollo // mission to the moon! // ... OK: I know this is corny, but geeks have to have some fun :-) // - Hopefully, this provides insight as to why there are multiple passes, // and allows you to follow the code more intuitively. //*--------------------------------------------------------------------------- //*--------------------------------------------------------------------------- // T Minus 3: Insure client code is no longer using the obsolete publicFace // built-in aspect // - OBSOLETE as of feature-u@1 // - NOTE: publicFace is still registered as a builtin for the // sole purpose of generating more specific error //*--------------------------------------------------------------------------- const featureNamesWithObsolete_publicFace = // locate features still using publicFace activeFeatures.filter( feature => feature.publicFace !== undefined ) .map( feature => feature.name ); if (featureNamesWithObsolete_publicFace.length > 0) { verify(false, `The OBSOLETE Feature.publicFace is still in-use in the following features: ${featureNamesWithObsolete_publicFace}\n` + '... as of feature-u@1 the publicFace builtin aspect has been replaced with fassets\n' + '... see: https://feature-u.js.org/cur/history.html#v1_0_0'); } //*--------------------------------------------------------------------------- // T Minus 2: Maintain our "active features" indicator // - in support of: fassets.hasFeature() //*--------------------------------------------------------------------------- activeFeatures.forEach( feature => { _hasFeature[feature.name] = true; }); //*--------------------------------------------------------------------------- // T Minus 1: Validate the basic structure of all Feature.fassets aspects // - including the fassets directives (define/defineUse/use) //*--------------------------------------------------------------------------- // filter features with the fassets aspect activeFeatures.filter( feature => feature.fassets !== undefined ) .forEach( feature => { { // HELP_EMACS: extra bracket - sorry to say, emacs web-mode can't handle indentation with nested filter/forEach (above) :-( const check = verify.prefix(`Feature.name: '${feature.name}' ... ERROR in "fassets" aspect: `); const fassets = feature.fassets; // fassets must be an object literal check(isPlainObject(fassets), `the fassets aspect MUST BE an object literal`); // insure all fassets directives are recognized const {define, use, defineUse, ...unknownDirectives} = fassets; // eslint-disable-line no-unused-vars const unknownDirectiveKeys = Object.keys(unknownDirectives); check(unknownDirectiveKeys.length === 0, `unrecognized fassets directive(s): ${unknownDirectiveKeys} ... expecting only: define/use/defineUse`); // verify at least ONE fassets directive is supplied // ... potential to relax this "empty" check // RELAXED: check(define!==undefined || use!==undefined || defineUse!==undefined, // `the fassets aspect is empty (at least one directive needed - define/use/defineUse)`); } // HELP_EMACS }); //*--------------------------------------------------------------------------- // Blast Off: Yeee Haaaa!! // - We are now off-the-ground //*--------------------------------------------------------------------------- //*--------------------------------------------------------------------------- // Stage 1: Interpret fasset "define"/"defineUse" directive, accumulating resources // - maintain _resources entries (with meta data) // - normalize resource directly in _fassets object // - validation: // * may contain federated namespace (with dots ".") // ... ex: 'MainPage.launch' // * MAY NOT contain wildcards // ... must be defined completely // * must be unique // ... cannot be defined more than once // ... these are individual "single-use" keys // In other words, we do NOT support the "pull" (bucket) philosophy // * NOTE: resource validation is postponed to subsequent stage // ... because we need BOTH _resources and _usage //*--------------------------------------------------------------------------- // filter features with the fassets aspect activeFeatures.filter( feature => feature.fassets !== undefined ) .forEach( feature => { { // HELP_EMACS const check = verify.prefix(`Feature.name: '${feature.name}' ... ERROR in "fassets" aspect, "define/defineUse" directive: `); const fassets = feature.fassets; // interpret BOTH define/defineUse directives // ... we attempt to process in same order defined within the fassets object literal // - by using Object.keys() // - HOWEVER: I do NOT believe this is universally possible (for all JS engines) // - In general, traversal of object literal property keys is NOT guaranteed // ... or at least the order may NOT be what the user expects (when key is interpreted as an integer) // ... see: http://2ality.com/2015/10/property-traversal-order-es6.html // - While this may work in a majority of cases, I DO NOT ADVERTISE THIS!!! const directiveKeys = Object.keys(fassets); directiveKeys.filter( directiveKey => directiveKey === 'define' || directiveKey === 'defineUse' ) // filter all define directives .forEach( directiveKey => { { // HELP_EMACS const defineDirective = fassets[directiveKey]; // validate that defineDirective is an object literal check(isPlainObject(defineDirective), `the ${directiveKey} directive MUST BE an object literal`); // verify at least ONE definition is supplied // ... potential to relax this "empty" check const resourceKeys = Object.keys(defineDirective); // RELAXED: check(resourceKeys.length > 0, `the ${directiveKey} directive is empty (at least one definition is needed)`); // iterpret each resource being defined // ... we attempt to process in same order defined within the fassets.define object literal // - ditto discussion (above) on processing order resourceKeys.forEach( resourceKey => { const resource = defineDirective[resourceKey]; // validate resource key // NOTE: resource validation is postponed to subsequent stage // ... because we need BOTH _resources and _usage // ... insure it will not overwrite our reserved fasset methods (get/hasFeature) check(resourceKey!=='get' && resourceKey!=='hasFeature', `fassets.${directiveKey}.'${resourceKey}' is a reserved word`); // ... restrict to a programmatic structure, because we normalize it (with depth) checkProgrammaticStruct(resourceKey, check); // ... must be unique (i.e. cannot be defined more than once) // ... these are individual "single-use" keys // In other words, we do NOT support the "pull" (bucket) philosophy const resourcePreviouslyDefined = _resources[resourceKey]; if (resourcePreviouslyDefined) // conditional required to prevent template literal (next line) from referencing undefined.definingFeature check(!resourcePreviouslyDefined, `fassets.${directiveKey}.'${resourceKey}' is NOT unique ... previously defined in Feature.name: '${resourcePreviouslyDefined.definingFeature}'`); // retain in _resources // ... NOTE: currently this structure is tested indirectly // - can't really export it without attaching it to the fassets object _resources[resourceKey] = { // ex: 'action.openView' ... NO wildcards allowed val: resource, // resource value definingFeature: feature.name, // the feature defining this resource defineUse: directiveKey === 'defineUse', // directive used in defining this resource (false: define. true: defineUse) }; // retain our key in our string blob (delimited by new-line) _fassetsKeysBlob += resourceKey + '\n'; // inject resource directly in our _fassets object (normalized) injectFassetsResource(resourceKey, resource, _fassets, check); }); } // HELP_EMACS }); } // HELP_EMACS }); // purge last cr/lf from _fassetsKeysBlob to prevent an open wildcard '*' // from returning a rogue empty string key ('') // ... causing an internal run-time error if (_fassetsKeysBlob !== '') { _fassetsKeysBlob = _fassetsKeysBlob.slice(0, -1); } //*--------------------------------------------------------------------------- // Stage 2: Interpret fasset "use" directive, accumulating usage contract // - maintain _usage entries // - interpret options directives (used in validation of optionality and type) // - validation: // * may contain federated namespace (with dots ".") // ... ex: 'MainPage.launch' // * may contain wildcards (with "*") // ... ex: 'MainPage.*.link' // * the uniqueness of "use" keys is NOT a requirement // ... IN OTHER WORDS: multiple features can specify the same (or overlapping) "use" keys // ... HOWEVER, for duplicate keys: // - the optionality can vary (required simply takes precedence) // - the expected data types MUST be the same // NOTE: For overlapping wildcard items, there is an opportunity to have // multiple expected types. This will be caught (indirectly) through // the resource validation (subsequent stage). //*--------------------------------------------------------------------------- // filter features with the "fassets" aspect -AND- "use" directive activeFeatures.filter( feature => feature.fassets !== undefined && feature.fassets.use !== undefined ) .forEach( feature => { { // HELP_EMACS const check = verify.prefix(`Feature.name: '${feature.name}' ... ERROR in "fassets" aspect, "use" directive: `); const useDirective = feature.fassets.use; // verify the use directive is an array check(Array.isArray(useDirective), `the use directive MUST BE an array`); // verify at least ONE usage contract is supplied // ... potential to relax this "empty" check // RELAXED: check(useDirective.length > 0, `the use directive is empty (at least one usage contract is needed`); // process each "use" contract useDirective.forEach( useEntry => { // decipher the useEntry, validating, and applying default semantics const {useKey, required, validateFn} = decipherDefaultedUseEntry(useEntry, check); // logf `processing fassets.use directive: '${useEntry}'\n:`, {useKey, required, validateFn} // maintain each use contract in our _usage object // NOTE: The uniqueness of "use" keys is NOT a requirement // IN OTHER WORDS: multiple features can specify the same (or overlapping) "use" keys // HOWEVER, for duplicate keys: // - the optionality can vary (required simply takes precedence) // - the expected data types MUST be the same // NOTE: For overlapping wildcard items, there is an opportunity to have // multiple expected types. This will be caught (indirectly) through // the resource validation (subsequent stage). if (_usage[useKey]) { // duplicate entry (from other features) // accumulate necessary items _usage[useKey].required = _usage[useKey].required || required; // required: true takes precedence _usage[useKey].definingFeatures.push(feature.name); // insure accumulation is possible check(_usage[useKey].validateFn===validateFn, `cannot accumulate duplicate 'use' contract from multiple features: [${_usage[useKey].definingFeatures}] ... the type validateFns are NOT the same`); } else { // initial entry (first time introduced) _usage[useKey] = { // ex: 'MainPage.*.link' ... wildcards allowed usingWildCard: containsWildCard(useKey), // does this usage contract employ wildcards? required, // optionality (required takes precedence for multiple entries) validateFn, // validation function (based on registered keywords) definingFeatures: [feature.name], // what feature(s) defined this "use" contract }; } // logf console.log(`_usage['${useKey}']:`, _usage[useKey]); }); } // HELP_EMACS }); //*--------------------------------------------------------------------------- // Stage 3: VALIDATION: Apply client-supplied validation constraints // >> this is done in our third stage, now that both resources and // usage contracts are in place (with it's validation constraints) // A: Apply client-supplied validation constraints // ... defined in the "use" directive // B: Insure "defineUse" resources match at least ONE usage contract // ... this is a fail-fast technique, quickly giving problem insight // to the client //*--------------------------------------------------------------------------- // accumulator of ALL validation errors, to show all at once const validationErrs = []; //*** // A: Apply client-supplied validation constraints // ... defined in the "use" directive //*** // Feature.fassets.use: "type" validation MyObj.entries(_resources) // resource iteration .forEach( ([fassetsKey, resource]) => { MyObj.entries(_usage) // "matching" usage contract iteration .filter( ([useKey, usage]) => isMatch(fassetsKey, createRegExp(useKey)) ) .forEach( ([useKey, usage]) => { { // HELP_EMACS // apply client-supplied validation constraints // NOTE: This also indirectly tests conflicts between overlapping usage contracts // ... i.e. two usage contracts (with varying wildcards) that both match an // entry can in fact specify different type checks const errStr = usage.validateFn(resource.val); if (errStr) { validationErrs.push(`VALIDATION ERROR in resource: '${fassetsKey}', expecting: ${errStr} ... resource defined in Feature: '${resource.definingFeature}', usage contract '${useKey}' found in Feature: '${usage.definingFeatures}'`); // EX: VALIDATION ERROR in resource: 'foo1', expecting: boolean ... resource defined in Feature: 'feature1', usage contract 'foo*' found in Feature: 'feature2' } } // HELP_EMACS }); }); // Feature.fassets.use: "required" validation MyObj.entries(_usage) // usage contract iteration .forEach( ([useKey, usage]) => { { // HELP_EMACS // when usage is contractually required, insure at least one matching resource is available if (usage.required) { const resource = _fassets.get(useKey); if (resource === undefined || // resource NOT found (usage.usingWildCard && resource.length === 0)) { // empty wildcard usage validationErrs.push(`REQUIRED RESOURCE NOT FOUND, usage contract '${useKey}' (found in Feature: '${usage.definingFeatures}') specifies a REQUIRED resource, but NO matches were found`); // EX: REQUIRED RESOURCE NOT FOUND, usage contract '*a.*.c*' (found in Feature: 'featureTest') specifies a REQUIRED resource, but NO matches were found } } } // HELP_EMACS }); //*** // B: Insure "defineUse" resources match at least ONE usage contract // ... this is a fail-fast technique, quickly giving problem insight to the client //*** MyObj.entries(_resources) // defineUse resource iteration .filter( ([fassetsKey, resource]) => resource.defineUse ) .forEach( ([fassetsKey, definedUseResource]) => { { // HELP_EMACS // fassetsKey must match at least one usage entry // ... because "defineUse" directives are intended to fulfill a use contract var matchFound = false; for (const useKey in _usage) { // usage contract iteration if (isMatch(fassetsKey, createRegExp(useKey))) { matchFound = true; break; } } // ERROR, when NO usage contracts match defineUse if (!matchFound) { validationErrs.push(`ERROR defineUse '${fassetsKey}' directive MUST match at least one usage contract, but does NOT ... is this misspelled? (found in Feature: '${definedUseResource.definingFeature}')`); // EX: ERROR defineUse 'wow.bbb' directive MUST match at least one usage contract, but does NOT ... is this misspelled? (found in Feature: 'feature1') } } // HELP_EMACS }); //*** //*** expose ALL validation issues in ONE Error //*** if (validationErrs.length===1) { // for a single error, just spit it out verify(false, validationErrs[0]); } else if (validationErrs.length>1) { // for multiple errors, combine all of them with a preamble verify(false, `${validationErrs.length} validation errors were found during Feature.fasset resource accumulation:\n` + validationErrs.join('\n')); } //*--------------------------------------------------------------------------- // OK: Our Saturn V is "now in orbit"!!! // - Ready for a trajectory to the moon // - INTERPRETATION: the fassets object is ready for: // - client consumption // - and seeding the withFassets() HoC //*--------------------------------------------------------------------------- // SideBar: To free up space, our regexp cache is deleted now that createFassets() is complete // ... because the fassets.get() maintains it's own results cache, // the regexps built up in this cache will rarely be needed (if at all) _regExpCache = {}; // log summary if (logf.isEnabled()) { const hookCount = activeFeatures.reduce( (count, feature) => feature.fassets ? count+1 : count, 0); const hookSummary = activeFeatures.map( (feature) => `\n Feature.name:${feature.name}${feature.fassets ? ' <-- defines: fassets' : ''}` ); logf(`cross-feature-communication ... INTERPRETING: Feature.fassets ... ${hookCount} hooks:${hookSummary}`); logf('cross-feature-communication ... fassets define/defineUse directives: ', _resources); // ... see WHO defined WHAT logf('cross-feature-communication ... fassets use directives: ', _usage); // ... see WHO enabled usage contracts logf('cross-feature-communication ... resolved fassets object: ', _fassets); // ... see consolated list of all fassets } // return our public fassets object (used in cross-feature-communication) return _fassets; } //****************************************************************************** //****************************************************************************** //* Internal Utility Functions //****************************************************************************** //****************************************************************************** /** * An internal function that injects the supplied key/val into obj, * normalized into a structure with depth (by interpreting the key's * federated namespace). * * @param {string} key the injection key. Can contain DOTs (.) which * will be normalized into a structure with depth. * * @param {Any} val the value to inject. * * @param {Object} obj the injection object. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @private */ function injectFassetsResource(key, val, obj, check) { const nodeKeys = key.split('.'); // interpret federated namespace (delimited with DOTs) const lastNode = nodeKeys.pop(); // extract last node (reducing the size of nodeKeys array) const runningObj = nodeKeys.reduce( (accum, nodeKey) => { if (accum[nodeKey]) { // appending to existing structure // ... must be a plain object ... otherwise it represents a conflict check(isPlainObject(accum[nodeKey]), `while normalizing the fassets '${key}' key, a conflict was detected with another feature at the '${nodeKey}' node (it is NOT an object)`); } else { // introduce new intermediate node accum[nodeKey] = {}; } return accum[nodeKey]; }, obj); // inject the val indexed by the lastNode // ... cannot clober (i.e. cover up or overwrite) existing data check(!runningObj[lastNode], `while normalizing the fassets '${key}' key, a conflict was detected with another feature at the '${lastNode}' node (overwriting existing data)`); runningObj[lastNode] = val; } /** * An internal function that validates supplied key, to be a * programmatic structure. * * feature-u's fassetKeys can be any JS identifier (less $ support - currently) * TODO: Omition of $ support was due to concern about conflict in wildcard processing (never actually tried). * * JavaScript Identifier Rules: * - MAY CONTAIN: alphas, digits, _, $ * - BEGINNING WITH: alphas, _, $ * - ARE: case-sensitive * - JavaScript keywords cannot be used * * Examples: * ``` * - allow embedded DOTS "." * - disallow wildcards "*" * - valid: "a" * "a1" * "a1.b" * "a1.b2.c" * "_a._b_.c_" * - invalid: "" // empty string * "123" // must start with alpha * ".a" // beginning empty string * "a." // ending empty string * "a..b" // embedded empty string * "a.b." // ending empty string (again) * "a.b.1" // each node must start with alpha * "a.b\n.c" // cr/lf NOT supported * "a.b .c" // spaces NOT supported * - wildcards: * "a.*.c" // depends on allowWildcards parameter * "*a.*.c*" // depends on allowWildcards parameter * ``` * * @param {string} key the key to validate. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @param {boolean} allowWildcards an indicator as to whether to * allow wildcards (true) or not (false DEFAULT). * * @private */ function checkProgrammaticStruct(key, check, allowWildcards=false) { const errMsg = `fassetsKey: '${key}' is invalid (NOT a JS identifier) ...`; const regExpAllowingWildcards = /^[_a-zA-Z\*][_a-zA-Z0-9\*]*$/; const regExpDisallowingWildcards = /^[_a-zA-Z][_a-zA-Z0-9]*$/; var regExpCheck = regExpAllowingWildcards; // insure NO cr/lf check(!isMatch(key, /[\n\r]/), `${errMsg} contains unsupported cr/lf`); // validate wildcards, per parameter // ... must also accomodate in our regexp check (below) but this check provides a more explicit message if (!allowWildcards) { check(!isMatch(key, /\*/), `${errMsg} wildcards are not supported`); regExpCheck = regExpDisallowingWildcards; } // analyze each node of the federated namespace const nodeKeys = key.split('.'); nodeKeys.forEach( nodeKey => { check(nodeKey!=='', `${errMsg} contains invalid empty string`); check(isMatch(nodeKey, regExpCheck), `${errMsg} each node must conform to a JS indentifier (less $ support)`); }); } /** * An internal function that deciphers the supplied useEntry, * validating, applying default semantics, and interpreting the two * formats: * * - a string * - a string/options in a two-element array * * @param {string-or-arrayOfStringOptionPairs} useEntry the use * entry to decipher. * * @param {assertionFn} check an assertion function (with context), * used to perform validation. * * @return {Object} defaulted object with following entries: {useKey, * required, validationFn} * * @private */ function decipherDefaultedUseEntry(useEntry, check) { // decipher various formats of useEntry const use = {}; if (isString(useEntry)) { use.useKey = useEntry; use.required = true; use.validateFn = fassetValidations.any; } else if (Array.isArray(useEntry)) { check(useEntry.length === 2, `"use" entry must either be a string or a string/options in a two-element array ... incorrect array size: ${useEntry.length}`); const [useKey, useOptions] = useEntry; check(isString(useKey), `"use" entry with options (two-element array), first element is NOT a string`); check(isPlainObject(useOptions), `"use" entry with options (two-element array), second element is NOT an object`); const {required = true, type:validateFn = fassetValidations.any, ...unknownOptions} = useOptions; const unknownOptionsKeys = Object.keys(unknownOptions); check(unknownOptionsKeys.length === 0, `"use" entry with options (two-element array), options have unrecognized entries: ${unknownOptionsKeys} ... expecting only: required/type`); use.useKey = useKey; use.required = required; use.validateFn = validateFn; } else { // unconditional error check(false, `"use" entry must either be a string or a string/options in a two-element array`); } // validate individual items // ... use.useKey checkProgrammaticStruct(use.useKey, check, true); // allowWildcards // ... use.required check(use.required===true || use.required===false, `"use" entry with options ('${use.useKey}'), 'required' entry must be true/false`); // ... use.validateFn check(isFunction(use.validateFn), `"use" entry with options ('${use.useKey}'), 'type' entry must be a fassetValidationFn`); // that's all folks return use; } /** * Return an indicator as to whether fassets-based wildcards are * present in supplied str. * * @param {string} str the string to check for wildcards. * * @return {boolean} true: wildcards present, false: no wildcards * detected * * @private */ export function containsWildCard(str) { // ... exported for unit tests only // check for fassets-based wildcards (only *) return str.includes('*'); } /** * Perform a regular expression search of the supplied string using * the regexp, returning all matches. * * @param {string} str the string to search. * * @param {RegExp} regexp the regular expression to match. * * @return {string[]} a string array of all matches, empty array for * no match. * * @private */ export function matchAll(str, regexp) { // ... exported for unit tests only // NOTE: Because we re-use our regexps (for optimization) // -AND- we use the "global" regexp modifier, // WE MUST RESET IT (so as to NOT pick up where it last left off) regexp.lastIndex = 0; return str.match(regexp) || []; // simple pass-through -BUT- convert null to empty array } /** * Return an indicator as to whether the supplied regexp has a match * in str. * * @param {string} str the string to search. * * @param {RegExp} regexp the regular expression to match. * * @return {boolean} true: match, false: no match * * @private */ export function isMatch(str, regexp) { // ... exported for unit tests only // NOTE: Because we re-use our regexps (for optimization) // -AND- we use the "global" regexp modifier, // WE MUST RESET IT (so as to NOT pick up where it last left off) regexp.lastIndex = 0; return regexp.test(str); // simple pass-through } // regexp cache used by createRegExp() // - optimizes repeated iteration in createFassets() "Stage 3: VALIDATION" // - SideBar: To free up space, this cache is deleted at end of createFassets() // ... the fassets.get() maintains results cache, // so regexp will be rarely needed (if at all) var _regExpCache = { /* dynamically maintained ... SAMPLE: 'MainPage.*.link': /^MainPage\..*\.link$/gm, 'selector.currentView': /^selector\.currentView$/gm, ... */ }; /** * Creates a fassets-specific regular expression from the supplied * pattern string, employing all the heuristics required by fassets * usage. * * @param {string} pattern the string to seed the regexp from. * * @return {RegExp} the newly created regexp. * * @private */ export function createRegExp(pattern) { // ... exported for unit tests only var wrkStr = pattern; // use cached value (when available) var regexp = _regExpCache[pattern]; if (regexp) { return regexp; } // convert all federated namespace delimiters (DOT ".") to their literal representation wrkStr = wrkStr.replace(/\./g, '\\.'); // convert all fasset wildcards ("*") to their RegExp equilivant (".*") wrkStr = wrkStr.replace(/\*/g, '.*'); // start/end anchors are required to match entire entry // ... NOTE: the user is in control (can disable by placing * at beginning or end) wrkStr = `^${wrkStr}$`; // construct the RegExp // ... in support of multi-line blob, we require modifiers (g: global, m: multiline) // NOTE: This has been manually tested to work in both our single and // multi-line cases (i.e. our blob). regexp = new RegExp(wrkStr, 'gm'); // maintain cache _regExpCache[pattern] = regexp; // that's all folks return regexp; }