Skip to content

Commit 9507de0

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

File tree

6 files changed

+243
-0
lines changed

6 files changed

+243
-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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ _.assign(SuperString.prototype, /** @lends SuperString.prototype */ {
9090
Substitutor = function (variables, defaults) {
9191
defaults && variables.push(defaults);
9292
this.variables = variables;
93+
94+
// tracks the lazy variables being used
95+
this.lazyVariables = [];
9396
};
9497

9598
_.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ {
@@ -153,6 +156,54 @@ _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ {
153156
// }
154157

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

@@ -224,6 +275,12 @@ _.assign(Substitutor, /** @lends Substitutor */ {
224275
return function (match, token) {
225276
var r = substitutor.find(token);
226277

278+
if (r && r.lazy) {
279+
substitutor.lazyVariables.push(r);
280+
281+
return match;
282+
}
283+
227284
r && _.isFunction(r) && (r = r());
228285
r && _.isFunction(r.toString) && (r = r.toString());
229286

test/unit/property.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,61 @@ 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+
});
465+
466+
it('should show variables as unresolved with values as async fn with error', async function () {
467+
const str = '{{world}}',
468+
variables = new VariableList(null, [
469+
{
470+
key: 'world',
471+
lazy: true,
472+
type: 'function',
473+
value: async () => {
474+
await new Promise((resolve) => {
475+
resolve('hello');
476+
});
477+
throw new Error('fail');
478+
}
479+
}
480+
]);
481+
482+
expect(await Property.replaceSubstitutionsLazy(str, variables)).to.eql('{{world}}');
483+
});
429484
});
430485

431486
describe('.replaceSubstitutionsIn', function () {
@@ -442,6 +497,30 @@ describe('Property', function () {
442497
});
443498
});
444499

500+
describe('.replaceSubstitutionsInLazy', function () {
501+
it('should replace with lazy variables', async function () {
502+
const obj = { foo: '{{var}}' },
503+
variables = new VariableList(null, [
504+
{
505+
key: 'var',
506+
type: 'any',
507+
lazy: true,
508+
value: async () => {
509+
const res = await new Promise((resolve) => {
510+
resolve('bar');
511+
});
512+
513+
return res;
514+
}
515+
}
516+
]),
517+
res = await Property.replaceSubstitutionsInLazy(obj, [variables]);
518+
519+
expect(res).to.eql({ foo: 'bar' });
520+
expect(obj).to.eql({ foo: '{{var}}' });
521+
});
522+
});
523+
445524
describe('variable resolution', function () {
446525
it('must resolve variables accurately', function () {
447526
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' },

test/unit/variable.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,4 +473,24 @@ describe('Variable', function () {
473473
expect(Variable.isVariable()).to.be.false;
474474
});
475475
});
476+
477+
describe('populate', function () {
478+
it('should populate and replace variable with key', async function () {
479+
const variable = new Variable({
480+
key: 'foo',
481+
value: async () => {
482+
const res = await new Promise((resolve) => {
483+
resolve('bar');
484+
});
485+
486+
return res;
487+
},
488+
type: 'lazy'
489+
});
490+
491+
await variable.populate();
492+
493+
expect(variable.toString()).to.eql('bar');
494+
});
495+
});
476496
});

0 commit comments

Comments
 (0)