Skip to content

Commit

Permalink
wip: add support for lazily replacing variables
Browse files Browse the repository at this point in the history
  • Loading branch information
Pranav Joglekar committed Nov 18, 2024
1 parent 2219c42 commit 1770756
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 15 deletions.
42 changes: 30 additions & 12 deletions lib/collection/property.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},

/**
Expand All @@ -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;
Expand All @@ -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; });
},

/**
Expand Down
24 changes: 24 additions & 0 deletions lib/collection/variable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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 {*}
Expand Down
52 changes: 50 additions & 2 deletions lib/superstring/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ {
Expand Down Expand Up @@ -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));

Expand All @@ -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 = [];
}
});

Expand Down Expand Up @@ -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;
};
}
Expand Down
59 changes: 58 additions & 1 deletion test/unit/property.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,14 +426,71 @@ 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 () {
it('should bail out if a non-object argument is passed', 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' }];

Expand Down
9 changes: 9 additions & 0 deletions test/unit/variable-scope.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down

0 comments on commit 1770756

Please sign in to comment.