Skip to content

Commit 7d554c9

Browse files
author
Pranav Joglekar
committed
feat: add support for async value functions
1 parent 2219c42 commit 7d554c9

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

lib/collection/property.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,26 @@ _.assign(Property, /** @lends Property */ {
286286
return Substitutor.box(variables, Substitutor.DEFAULT_VARS).parse(str).toString();
287287
},
288288

289+
/**
290+
* Similar to `replaceSubstitutions` but runs asynchronously
291+
* and supports async value functions
292+
*
293+
* @param {String} str -
294+
* @param {VariableList|Object|Array.<VariableList|Object>} variables -
295+
* @returns {String}
296+
*/
297+
// @todo: improve algorithm via variable replacement caching
298+
replaceSubstitutionsLazy: async function (str, variables) {
299+
// if there is nothing to replace, we move on
300+
if (!(str && _.isString(str))) { return str; }
301+
302+
// if variables object is not an instance of substitutor then ensure that it is an array so that it becomes
303+
// compatible with the constructor arguments for a substitutor
304+
!Substitutor.isInstance(variables) && !_.isArray(variables) && (variables = _.tail(arguments));
305+
306+
return (await Substitutor.box(variables, Substitutor.DEFAULT_VARS).parseLazy(str)).toString();
307+
},
308+
289309
/**
290310
* This function accepts an object followed by a number of variable sources as arguments. One or more variable
291311
* sources can be provided and it will use the one that has the value in left-to-right order.
@@ -319,6 +339,49 @@ _.assign(Property, /** @lends Property */ {
319339
return _.mergeWith({}, obj, customizer);
320340
},
321341

342+
/**
343+
* Similar to `replaceSubstitutionsIn` but runs asynchronously
344+
* and supports async value functions
345+
*
346+
* @param {Object} obj -
347+
* @param {Array.<VariableList|Object>} variables -
348+
* @returns {Object}
349+
*/
350+
replaceSubstitutionsInLazy: async function (obj, variables) {
351+
// if there is nothing to replace, we move on
352+
if (!(obj && _.isObject(obj))) {
353+
return obj;
354+
}
355+
356+
// convert the variables to a substitutor object (will not reconvert if already substitutor)
357+
variables = Substitutor.box(variables, Substitutor.DEFAULT_VARS);
358+
359+
const promises = [];
360+
var customizer = function (objectValue, sourceValue, key) {
361+
objectValue = objectValue || {};
362+
if (!_.isString(sourceValue)) {
363+
_.forOwn(sourceValue, function (value, key) {
364+
sourceValue[key] = customizer(objectValue[key], value);
365+
});
366+
367+
return sourceValue;
368+
}
369+
370+
const result = this.replaceSubstitutionsLazy(sourceValue, variables);
371+
372+
promises.push({ key: key, promise: result });
373+
374+
return result;
375+
}.bind(this),
376+
res = _.mergeWith({}, obj, customizer);
377+
378+
await Promise.all(promises.map(async ({ key, promise }) => {
379+
res[key] = await promise;
380+
}));
381+
382+
return res;
383+
},
384+
322385
/**
323386
* This function recursively traverses a variable and detects all instances of variable replacements
324387
* within the string of the object

lib/collection/variable.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ _.assign(Variable.prototype, /** @lends Variable.prototype */ {
122122
return (!_.isNil(value) && _.isFunction(value.toString)) ? value.toString() : E;
123123
},
124124

125+
/**
126+
* Runs the value function and updates the variable to store
127+
* the return value of the function
128+
*
129+
* @returns {String}
130+
*/
131+
async populate () {
132+
const value = await this.valueOf();
133+
134+
this.valueOf(value);
135+
this.valueType(typeof value);
136+
this.lazy = false;
137+
},
138+
125139
/**
126140
* Typecasts a value to the {@link Variable.types} of this {@link Variable}. Returns the value of the variable
127141
* converted to the type specified in {@link Variable#type}.
@@ -208,6 +222,7 @@ _.assign(Variable.prototype, /** @lends Variable.prototype */ {
208222
_.has(options, 'system') && (this.system = options.system);
209223
_.has(options, 'disabled') && (this.disabled = options.disabled);
210224
_.has(options, 'description') && (this.describe(options.description));
225+
_.has(options, 'lazy') && (this.lazy = options.lazy);
211226
}
212227
});
213228

lib/superstring/index.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ _.assign(SuperString.prototype, /** @lends SuperString.prototype */ {
9090
Substitutor = function (variables, defaults) {
9191
defaults && variables.push(defaults);
9292
this.variables = variables;
93+
this.lazyResolutions = [];
9394
};
9495

9596
_.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ {
@@ -153,6 +154,48 @@ _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ {
153154
// }
154155

155156
return value;
157+
},
158+
159+
/**
160+
* @param {SuperString} value -
161+
* @returns {String}
162+
*/
163+
async parseLazy (value) {
164+
// convert the value into a SuperString so that it can return tracking results during replacements
165+
value = new SuperString(value);
166+
167+
// get an instance of a replacer function that would be used to replace ejs like variable replacement
168+
// tokens
169+
var replacer = Substitutor.replacer(this);
170+
171+
// replace the value once and keep on doing it until all tokens are replaced or we have reached a limit of
172+
// replacements
173+
do {
174+
if (this.lazyResolutions.length) {
175+
// eslint-disable-next-line no-await-in-loop
176+
await this.populate();
177+
}
178+
value = value.replace(Substitutor.REGEX_EXTRACT_VARS, replacer);
179+
} while (value.replacements && (value.substitutions < Substitutor.VARS_SUBREPLACE_LIMIT));
180+
181+
// @todo: uncomment this code, and try to raise a warning in some way.
182+
// do a final check that if recursion limits are reached then replace with blank string
183+
// if (value.substitutions >= Substitutor.VARS_SUBREPLACE_LIMIT) {
184+
// value = value.replace(Substitutor.REGEX_EXTRACT_VARS, E);
185+
// }
186+
187+
return value.toString();
188+
},
189+
190+
async populate () {
191+
await Promise.all(this.lazyResolutions.map(async (lazyResolution) => {
192+
let r = lazyResolution;
193+
194+
r && _.isFunction(r) && (r = await r());
195+
r && _.isFunction(r.populate) && (await r.populate());
196+
}));
197+
198+
this.lazyResolutions = [];
156199
}
157200
});
158201

@@ -224,6 +267,12 @@ _.assign(Substitutor, /** @lends Substitutor */ {
224267
return function (match, token) {
225268
var r = substitutor.find(token);
226269

270+
if (r && r.lazy) {
271+
substitutor.lazyResolutions.push(r);
272+
273+
return match;
274+
}
275+
227276
r && _.isFunction(r) && (r = r());
228277
r && _.isFunction(r.toString) && (r = r.toString());
229278

test/unit/property.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,42 @@ describe('Property', function () {
426426
// resolves all independent unique variables as well as poly-chained {{0}} & {{1}}
427427
expect(Property.replaceSubstitutions(str, variables)).to.eql('{{xyz}}');
428428
});
429+
430+
it('should correctly resolve variables with values as sync fn', function () {
431+
const str = '{{world}}',
432+
variables = new VariableList(null, [
433+
{
434+
key: 'world',
435+
value: () => {
436+
return 'hello';
437+
}
438+
}
439+
]);
440+
441+
expect(Property.replaceSubstitutions(str, variables)).to.eql('hello');
442+
});
443+
});
444+
445+
describe('.replaceSubstitionsLazy', function () {
446+
it('should correctly resolve variables with values as async fn', async function () {
447+
const str = '{{world}}',
448+
variables = new VariableList(null, [
449+
{
450+
key: 'world',
451+
lazy: true,
452+
type: 'function',
453+
value: async () => {
454+
const x = await new Promise((resolve) => {
455+
resolve('hello');
456+
});
457+
458+
return x;
459+
}
460+
}
461+
]);
462+
463+
expect(await Property.replaceSubstitutionsLazy(str, variables)).to.eql('hello');
464+
});
429465
});
430466

431467
describe('.replaceSubstitutionsIn', function () {
@@ -442,6 +478,30 @@ describe('Property', function () {
442478
});
443479
});
444480

481+
describe('.replaceSubstitutionsInLazy', function () {
482+
it('should replace with lazy variables', async function () {
483+
const obj = { foo: '{{var}}' },
484+
variables = new VariableList(null, [
485+
{
486+
key: 'var',
487+
type: 'any',
488+
lazy: true,
489+
value: async () => {
490+
const res = await new Promise((resolve) => {
491+
resolve('bar');
492+
});
493+
494+
return res;
495+
}
496+
}
497+
]),
498+
res = await Property.replaceSubstitutionsInLazy(obj, [variables]);
499+
500+
expect(res).to.eql({ foo: 'bar' });
501+
expect(obj).to.eql({ foo: '{{var}}' });
502+
});
503+
});
504+
445505
describe('variable resolution', function () {
446506
it('must resolve variables accurately', function () {
447507
var unresolvedRequest = {

test/unit/variable-scope.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ describe('VariableScope', function () {
299299
expect(scope.get('var-2')).to.equal('var-2-value');
300300
});
301301

302+
it('should get the specified variable with value as a fn', function () {
303+
var scope = new VariableScope([
304+
{ key: 'var-1', value: () => { return 'var-1-value'; } },
305+
{ key: 'var-2', value: () => { return 'var-2-value'; } }
306+
]);
307+
308+
expect(scope.get('var-2')).to.equal('var-2-value');
309+
});
310+
302311
it('should get last enabled from multi value list', function () {
303312
var scope = new VariableScope([
304313
{ key: 'var-2', value: 'var-2-value' },

0 commit comments

Comments
 (0)