Skip to content

feat: json variable access (VF-3664) #293

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/common/src/constants/regexp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const SLOT_ANNOTATION_SIMPLE_REGEX = /{([^ .[\]{}]+?)}/g;

export const IS_VARIABLE_REGEXP = /^{.*}$/;

export const READABLE_VARIABLE_REGEXP = /{(\w{1,64})}/g;
// export const READABLE_VARIABLE_REGEXP = /{(\w{1,64})}/g;
export const READABLE_VARIABLE_REGEXP = /{(\w{1,64})((?:\.\w{1,64}|\[\d+])*)}/gi;

export const VALID_CHARACTER = 'a-zA-Z';

Expand Down
33 changes: 29 additions & 4 deletions packages/common/src/utils/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,37 @@ import { READABLE_VARIABLE_REGEXP } from '@common/constants';
export const variableReplacer = (
match: string,
inner: string,
selectors: string[],
variables: Record<string, unknown>,
modifier?: (variable: unknown) => unknown
): unknown => {
if (inner in variables) {
return typeof modifier === 'function' ? modifier(variables[inner]) : variables[inner];
if (!(inner in variables)) {
return match;
}

return match;
let replaced: any = variables[inner];

let selectorString = selectors[0];
while (selectorString.length > 0) {
// eslint-disable-next-line no-loop-func
selectorString = selectorString.replace(/^\.(\w{1,64})/, (_m, field) => {
replaced = replaced[field];
return '';
});
if (replaced === undefined) {
break;
}
// eslint-disable-next-line no-loop-func
selectorString = selectorString.replace(/^\[(\d+)]/, (_m, index) => {
replaced = replaced[index];
return '';
});
if (replaced === undefined) {
break;
}
}

return typeof modifier === 'function' ? modifier(replaced) : replaced;
};

export const replaceVariables = (
Expand All @@ -23,7 +46,9 @@ export const replaceVariables = (
return '';
}

return phrase.replace(READABLE_VARIABLE_REGEXP, (match, inner) => String(variableReplacer(match, inner, variables, modifier)));
return phrase.replace(READABLE_VARIABLE_REGEXP, (match, inner, ...selectors) =>
String(variableReplacer(match, inner, selectors, variables, modifier))
);
};

// turn float variables to 2 decimal places
Expand Down
50 changes: 50 additions & 0 deletions packages/common/tests/utils/variables.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { replaceVariables } from '@common/utils/variables';
import { expect } from 'chai';

describe('Utils | variables', () => {
describe('replaceVariables', () => {
const variables = {
name: 'bob',
age: 32,
favFoods: ['takoyaki', 'onigiri', 'taiyaki'],
job: {
company: 'voiceflow',
position: 'software engineer',
team: 'creator',
},
};
it('correctly replaces simple variables', () => {
expect(replaceVariables('hello, my name is {name}, and i am {age} years old', variables)).to.eq('hello, my name is bob, and i am 32 years old');
expect(replaceVariables('{name} {name} {name}', variables)).to.eq('bob bob bob');
});
it('variables that are not defined do not get expanded', () => {
expect(replaceVariables('hello, my name is {name} and i work at {workplace}', variables)).to.eq(
'hello, my name is bob and i work at {workplace}'
);
expect(replaceVariables('hello, my name is {Name}', variables)).to.eq('hello, my name is {Name}');
});

it('array access works', () => {
expect(replaceVariables('most favorite food is {favFoods[0]}, second favorite is {favFoods[1]}, and third is {favFoods[2]}', variables)).to.eq(
'most favorite food is takoyaki, second favorite is onigiri, and third is taiyaki'
);
});
// it('index out of range', () => {});

it('object access works', () => {
expect(replaceVariables('i work at {job.company} as a {job.position} on the {job.team} team', variables)).to.eq(
'i work at voiceflow as a software engineer on the creator team'
);
});
// it('non-existent fields', () => {});

it('weird cases', () => {
const variables = {
'': 6969,
var: '{name}',
};
expect(replaceVariables('this is a blank variable {}', variables)).to.eq('this is a blank variable {}');
expect(replaceVariables('{var}', variables)).to.eq('{name}');
});
});
});