From 17707563c2a3de5223614f3e4f85c1f82fc317a6 Mon Sep 17 00:00:00 2001 From: Pranav Joglekar Date: Mon, 18 Nov 2024 15:21:59 +0530 Subject: [PATCH] wip: add support for lazily replacing variables --- lib/collection/property.js | 42 ++++++++++++++++------- lib/collection/variable.js | 24 +++++++++++++ lib/superstring/index.js | 52 ++++++++++++++++++++++++++-- test/unit/property.test.js | 59 +++++++++++++++++++++++++++++++- test/unit/variable-scope.test.js | 9 +++++ 5 files changed, 171 insertions(+), 15 deletions(-) diff --git a/lib/collection/property.js b/lib/collection/property.js index 18f8cf819..53cda3b71 100644 --- a/lib/collection/property.js +++ b/lib/collection/property.js @@ -283,7 +283,7 @@ _.assign(Property, /** @lends Property */ { // compatible with the constructor arguments for a substitutor !Substitutor.isInstance(variables) && !_.isArray(variables) && (variables = _.tail(arguments)); - return Substitutor.box(variables, Substitutor.DEFAULT_VARS).parse(str).toString(); + return Substitutor.box(variables, Substitutor.DEFAULT_VARS).parse(str); }, /** @@ -295,6 +295,10 @@ _.assign(Property, /** @lends Property */ { * @returns {Object} */ replaceSubstitutionsIn: function (obj, variables) { + return this.lazyReplaceSubstitutionsIn(obj, variables); + }, + + lazyReplaceSubstitutionsIn: function (obj, variables) { // if there is nothing to replace, we move on if (!(obj && _.isObject(obj))) { return obj; @@ -303,20 +307,34 @@ _.assign(Property, /** @lends Property */ { // convert the variables to a substitutor object (will not reconvert if already substitutor) variables = Substitutor.box(variables, Substitutor.DEFAULT_VARS); - var customizer = function (objectValue, sourceValue) { - objectValue = objectValue || {}; - if (!_.isString(sourceValue)) { - _.forOwn(sourceValue, function (value, key) { - sourceValue[key] = customizer(objectValue[key], value); - }); + const promises = []; + var customizer = function (objectValue, sourceValue, key) { + objectValue = objectValue || {}; + if (!_.isString(sourceValue)) { + _.forOwn(sourceValue, function (value, key) { + sourceValue[key] = customizer(objectValue[key], value); + }); - return sourceValue; - } + return sourceValue; + } + + const result = this.replaceSubstitutions(sourceValue, variables); - return this.replaceSubstitutions(sourceValue, variables); - }.bind(this); + if (result.then) { + promises.push({ key: key, promise: result }); + } + + return result; + }.bind(this), + res = _.mergeWith({}, obj, customizer); + + if (promises.length === 0) { + return res; + } - return _.mergeWith({}, obj, customizer); + return Promise.all(promises.map(async ({ key, promise }) => { + res[key] = await promise; + })).then(() => { return res; }); }, /** diff --git a/lib/collection/variable.js b/lib/collection/variable.js index cac0a041f..3ce5a591c 100644 --- a/lib/collection/variable.js +++ b/lib/collection/variable.js @@ -122,6 +122,13 @@ _.assign(Variable.prototype, /** @lends Variable.prototype */ { return (!_.isNil(value) && _.isFunction(value.toString)) ? value.toString() : E; }, + async populate () { + const value = await this.valueOf(); + + this.valueOf(value); + this.valueType(typeof value); + }, + /** * Typecasts a value to the {@link Variable.types} of this {@link Variable}. Returns the value of the variable * converted to the type specified in {@link Variable#type}. @@ -355,6 +362,23 @@ _.assign(Variable, /** @lends Variable */ { return val; // pass through }, + /** + * @param {*} val - + * @returns {*} + */ + out (val) { + return val; // pass through + } + }, + lazy: { + /** + * @param {*} val - + * @returns {*} + */ + in (val) { + return val; // pass through + }, + /** * @param {*} val - * @returns {*} diff --git a/lib/superstring/index.js b/lib/superstring/index.js index d8894b74e..3daea8459 100644 --- a/lib/superstring/index.js +++ b/lib/superstring/index.js @@ -90,6 +90,7 @@ _.assign(SuperString.prototype, /** @lends SuperString.prototype */ { Substitutor = function (variables, defaults) { defaults && variables.push(defaults); this.variables = variables; + this.lazyResolutions = []; }; _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ { @@ -143,6 +144,11 @@ _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ { // replace the value once and keep on doing it until all tokens are replaced or we have reached a limit of // replacements do { + if (this.lazyResolutions.length) { + // eslint-disable-next-line no-await-in-loop + // await this.populate(); + return this.parseLazy(value, replacer); + } value = value.replace(Substitutor.REGEX_EXTRACT_VARS, replacer); } while (value.replacements && (value.substitutions < Substitutor.VARS_SUBREPLACE_LIMIT)); @@ -152,7 +158,42 @@ _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ { // value = value.replace(Substitutor.REGEX_EXTRACT_VARS, E); // } - return value; + return value.toString(); + }, + + /** + * @param {SuperString} value - + * @param {Function} replacer - + * @returns {String} + */ + async parseLazy (value, replacer) { + // replace the value once and keep on doing it until all tokens are replaced or we have reached a limit of + // replacements + do { + if (this.lazyResolutions.length) { + // eslint-disable-next-line no-await-in-loop + await this.populate(); + } + value = value.replace(Substitutor.REGEX_EXTRACT_VARS, replacer); + } while (value.replacements && (value.substitutions < Substitutor.VARS_SUBREPLACE_LIMIT)); + + // @todo: uncomment this code, and try to raise a warning in some way. + // do a final check that if recursion limits are reached then replace with blank string + // if (value.substitutions >= Substitutor.VARS_SUBREPLACE_LIMIT) { + // value = value.replace(Substitutor.REGEX_EXTRACT_VARS, E); + // } + + return value.toString(); + }, + async populate () { + await Promise.all(this.lazyResolutions.map(async (lazyResolution) => { + let { r } = lazyResolution; + + r && _.isFunction(r) && (r = await r()); + r && _.isFunction(r.populate) && (await r.populate()); + })); + + this.lazyResolutions = []; } }); @@ -221,12 +262,19 @@ _.assign(Substitutor, /** @lends Substitutor */ { * @returns {Function} */ replacer: function (substitutor) { - return function (match, token) { + return function (match, token, offset) { var r = substitutor.find(token); + if (r && r.type === 'lazy') { + substitutor.lazyResolutions.push({ match, offset, r }); + + return match; + } + r && _.isFunction(r) && (r = r()); r && _.isFunction(r.toString) && (r = r.toString()); + const updated_r = substitutor.find(token); return Substitutor.NATIVETYPES[(typeof r)] ? r : match; }; } diff --git a/test/unit/property.test.js b/test/unit/property.test.js index 45f4c0b88..2c9387e98 100644 --- a/test/unit/property.test.js +++ b/test/unit/property.test.js @@ -426,6 +426,39 @@ describe('Property', function () { // resolves all independent unique variables as well as poly-chained {{0}} & {{1}} expect(Property.replaceSubstitutions(str, variables)).to.eql('{{xyz}}'); }); + + it('should correctly resolve variables with values as sync fn', async function () { + const str = '{{world}}', + variables = new VariableList(null, [ + { + key: 'world', + value: () => { + return 'hello'; + } + } + ]); + + expect(Property.replaceSubstitutions(str, variables)).to.eql('hello'); + }); + + it('should correctly resolve variables with values as async fn', async function () { + const str = '{{world}}', + variables = new VariableList(null, [ + { + key: 'world', + type: 'lazy', + value: async () => { + const x = await new Promise((resolve) => { + resolve('hello'); + }); + + return x; + } + } + ]); + + expect(await Property.replaceSubstitutions(str, variables)).to.eql('hello'); + }); }); describe('.replaceSubstitutionsIn', function () { @@ -433,7 +466,31 @@ describe('Property', function () { expect(Property.replaceSubstitutionsIn('random')).to.equal('random'); }); - it('should not mutate the original object', function () { + it('should not mutate the original object', async function () { + const obj = { foo: '{{var}}' }, + variables = new VariableList(null, [ + { + key: 'var', + type: 'lazy', + value: () => { + /* + const x = await new Promise((resolve) => { + resolve('bar'); + }); + */ + + return 'bar'; + } + } + ]); + + const res = await Property.replaceSubstitutionsIn(obj, [variables]); + console.log({ res }); + expect(res).to.eql({ foo: 'bar' }); + expect(obj).to.eql({ foo: '{{var}}' }); + }); + + it('should replace with lazy variables', async function () { const obj = { foo: '{{var}}' }, variables = [{ var: 'bar' }]; diff --git a/test/unit/variable-scope.test.js b/test/unit/variable-scope.test.js index aee0f53f6..0eaeb5d9b 100644 --- a/test/unit/variable-scope.test.js +++ b/test/unit/variable-scope.test.js @@ -299,6 +299,15 @@ describe('VariableScope', function () { expect(scope.get('var-2')).to.equal('var-2-value'); }); + it('should get the specified variable with value as a fn', function () { + var scope = new VariableScope([ + { key: 'var-1', value: () => { return 'var-1-value'; } }, + { key: 'var-2', value: () => { return 'var-2-value'; } } + ]); + + expect(scope.get('var-2')).to.equal('var-2-value'); + }); + it('should get last enabled from multi value list', function () { var scope = new VariableScope([ { key: 'var-2', value: 'var-2-value' },