Skip to content

Commit 46fac10

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

File tree

4 files changed

+219
-0
lines changed

4 files changed

+219
-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+
replaceSubstitutionsAsync: 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).parseAsync(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+
replaceSubstitutionsInAsync: 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.replaceSubstitutionsAsync(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/superstring/index.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,46 @@ _.assign(SuperString.prototype, /** @lends SuperString.prototype */ {
6464
return this;
6565
},
6666

67+
async replaceAsync (regex, fn) {
68+
var replacements = 0; // maintain a count of tokens replaced
69+
70+
// to ensure we do not perform needless operations in the replacement, we use multiple replacement functions
71+
// after validating the parameters
72+
const replacerFn = _.isFunction(fn) ?
73+
function () {
74+
replacements += 1;
75+
76+
return fn.apply(this, arguments);
77+
} :
78+
// this case is returned when replacer is not a function (ensures we do not need to check it)
79+
/* istanbul ignore next */
80+
function () {
81+
replacements += 1;
82+
83+
return fn;
84+
};
85+
86+
let index = 0,
87+
match;
88+
89+
while ((match = regex.exec(this.value.slice(index)))) {
90+
try {
91+
// eslint-disable-next-line no-await-in-loop
92+
let value = await replacerFn(...match);
93+
94+
index += match.index;
95+
this.value = this.value.slice(0, index) + value + this.value.slice(index + match[0].length);
96+
index += match[0].length;
97+
}
98+
catch (_err) { /* empty */ }
99+
}
100+
101+
this.replacements = replacements; // store the last replacements
102+
replacements && (this.substitutions += 1); // if any replacement is done, count that some substitution was made
103+
104+
return this;
105+
},
106+
67107
/**
68108
* @returns {String}
69109
*/
@@ -153,6 +193,34 @@ _.assign(Substitutor.prototype, /** @lends Substitutor.prototype */ {
153193
// }
154194

155195
return value;
196+
},
197+
198+
/**
199+
* @param {SuperString} value -
200+
* @returns {String}
201+
*/
202+
async parseAsync (value) {
203+
// convert the value into a SuperString so that it can return tracking results during replacements
204+
value = new SuperString(value);
205+
206+
// get an instance of a replacer function that would be used to replace ejs like variable replacement
207+
// tokens
208+
var replacer = Substitutor.replacer(this);
209+
210+
// replace the value once and keep on doing it until all tokens are replaced or we have reached a limit of
211+
// replacements
212+
do {
213+
// eslint-disable-next-line no-await-in-loop
214+
value = await value.replaceAsync(Substitutor.REGEX_EXTRACT_VARS, replacer);
215+
} while (value.replacements && (value.substitutions < Substitutor.VARS_SUBREPLACE_LIMIT));
216+
217+
// @todo: uncomment this code, and try to raise a warning in some way.
218+
// do a final check that if recursion limits are reached then replace with blank string
219+
// if (value.substitutions >= Substitutor.VARS_SUBREPLACE_LIMIT) {
220+
// value = value.replace(Substitutor.REGEX_EXTRACT_VARS, E);
221+
// }
222+
223+
return value.toString();
156224
}
157225
});
158226

@@ -225,6 +293,9 @@ _.assign(Substitutor, /** @lends Substitutor */ {
225293
var r = substitutor.find(token);
226294

227295
r && _.isFunction(r) && (r = r());
296+
if (r && _.isFunction(r.value)) {
297+
return r.get();
298+
}
228299
r && _.isFunction(r.toString) && (r = r.toString());
229300

230301
return Substitutor.NATIVETYPES[(typeof r)] ? r : match;

test/unit/property.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,59 @@ 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('.replaceSubstitutionsAsync', 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+
type: 'function',
452+
value: async () => {
453+
const x = await new Promise((resolve) => {
454+
resolve('hello');
455+
});
456+
457+
return x;
458+
}
459+
}
460+
]);
461+
462+
expect(await Property.replaceSubstitutionsAsync(str, variables)).to.eql('hello');
463+
});
464+
465+
it('should show variables as unresolved with values as async fn with error', async function () {
466+
const str = '{{world}}',
467+
variables = new VariableList(null, [
468+
{
469+
key: 'world',
470+
type: 'function',
471+
value: async () => {
472+
await new Promise((resolve) => {
473+
resolve('hello');
474+
});
475+
throw new Error('fail');
476+
}
477+
}
478+
]);
479+
480+
expect(await Property.replaceSubstitutionsAsync(str, variables)).to.eql('{{world}}');
481+
});
429482
});
430483

431484
describe('.replaceSubstitutionsIn', function () {
@@ -442,6 +495,29 @@ describe('Property', function () {
442495
});
443496
});
444497

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