json-templates
Version:
Simple JSON value templating.
745 lines (667 loc) • 22.8 kB
JavaScript
// These are the unit tests for the library.
// Run them with the command `npm test`.
// By Curran Kelleher
// September 2016
// tests for duplication/deduplication added by Paul Brewer, Economic & Financial Technology Consulting LLC, Dec 2017
const assert = require('assert');
const parse = require('./dist');
describe('json-template', () => {
// Handling of strings is the most critical part of the functionality.
// This section tests the string templating functionality,
// including default values and edge cases.
describe('strings', () => {
it('should compute template for a string with a single parameter', () => {
const template = parse('{{foo}}');
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.equal(template({ foo: 'bar' }), 'bar');
});
it('should compute template for a string with a nested object parameter', () => {
const template = parse('{{foo.value:baz}}');
assert.deepEqual(template.parameters, [
{ key: 'foo.value', defaultValue: 'baz' },
]);
assert.equal(template({ foo: { value: 'bar' } }), 'bar');
assert.equal(template(), 'baz');
});
it('should compute template for a string as raw key with rawKey option', () => {
const template = parse('{{foo.value:baz}}', { rawKey: true });
assert.deepEqual(template.parameters, [
{ key: 'foo.value', defaultValue: 'baz' },
]);
assert.equal(template({ 'foo.value': 'bar' }), 'bar');
assert.equal(template(), 'baz');
});
it('should compute template for strings with no parameters', () => {
['foo', '{{}}', '}}{{', '}}foo{{'].forEach(function (value) {
const template = parse(value);
assert.deepEqual(template.parameters, []);
assert.equal(template(), value);
});
});
it('should compute template with default for a string', () => {
const template = parse('{{foo:bar}}');
assert.deepEqual(template.parameters, [
{
key: 'foo',
defaultValue: 'bar',
},
]);
assert.equal(template(), 'bar');
assert.equal(template({ foo: 'baz' }), 'baz');
assert.equal(template({ unknownParam: 'baz' }), 'bar');
});
it('should compute template with default for a string with multiple colons', () => {
const template = parse('{{foo:bar:baz}}');
assert.deepEqual(template.parameters, [
{
key: 'foo',
defaultValue: 'bar:baz',
},
]);
assert.equal(template(), 'bar:baz');
assert.equal(template({ foo: 'baz' }), 'baz');
assert.equal(template({ unknownParam: 'baz' }), 'bar:baz');
});
it('should compute template for a string with inner parameter', () => {
const template = parse('Hello {{foo}}, how are you ?');
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.equal(template({ foo: 'john' }), 'Hello john, how are you ?');
});
it('should compute template for a string with multiple inner parameters', () => {
const template = parse('Hello {{firstName}} {{lastName}}, how are you ?');
assert.deepEqual(template.parameters, [
{ key: 'firstName' },
{ key: 'lastName' },
]);
assert.equal(
template({ firstName: 'Jane', lastName: 'Doe' }),
'Hello Jane Doe, how are you ?',
);
});
it('should handle extra whitespace', () => {
const template = parse(
'Hello {{firstName }} {{ lastName}}, how are you ?',
);
assert.deepEqual(template.parameters, [
{ key: 'firstName' },
{ key: 'lastName' },
]);
assert.equal(
template({ firstName: 'Jane', lastName: 'Doe' }),
'Hello Jane Doe, how are you ?',
);
});
it('should handle dashes in defaults', () => {
const template = parse('{{startTime:now-24h}}');
assert.deepEqual(template.parameters, [
{ key: 'startTime', defaultValue: 'now-24h' },
]);
assert.equal(template({ startTime: 'now-48h' }), 'now-48h');
assert.equal(template(), 'now-24h');
});
it('should handle special characters in defaults', () => {
const template = parse('{{foo:-+., @/()?=*_}}');
assert.deepEqual(template.parameters, [
{ key: 'foo', defaultValue: '-+., @/()?=*_' },
]);
assert.equal(template({ foo: '-+., @/()?=*_' }), '-+., @/()?=*_');
assert.equal(template(), '-+., @/()?=*_');
});
it('should handle email address in defaults', () => {
const template = parse('{{email:jdoe@mail.com}}');
assert.deepEqual(template.parameters, [
{ key: 'email', defaultValue: 'jdoe@mail.com' },
]);
assert.equal(template({ email: 'jdoe@mail.com' }), 'jdoe@mail.com');
assert.equal(template(), 'jdoe@mail.com');
});
it('should handle phone number in defaults', () => {
const template = parse('{{phone:+1 (256) 34-34-4556}}');
assert.deepEqual(template.parameters, [
{ key: 'phone', defaultValue: '+1 (256) 34-34-4556' },
]);
assert.equal(
template({ phone: '+1 (256) 34-34-4556' }),
'+1 (256) 34-34-4556',
);
assert.equal(template(), '+1 (256) 34-34-4556');
});
it('should handle url in defaults', () => {
const template = parse('{{url:http://www.host.com/path?key_1=value}}');
assert.deepEqual(template.parameters, [
{ key: 'url', defaultValue: 'http://www.host.com/path?key_1=value' },
]);
assert.equal(
template({ url: 'http://www.host.com/path?key_1=value' }),
'http://www.host.com/path?key_1=value',
);
assert.equal(template(), 'http://www.host.com/path?key_1=value');
});
it('should handle empty strings for parameter value', () => {
const template = parse('{{foo}}');
assert.equal(template({ foo: '' }), '');
});
it('should handle null and undefined as empty strings for parameter value', () => {
const template = parse('{{foo}} {{bar}}');
assert.equal(template({ foo: null }), ' ');
});
it('number variable inside string should be replaced', () => {
const result = parse('abc{{a}}def')({ a: 1 });
assert.equal(result, 'abc1def');
});
});
// This section tests that the parse function recursively
// traverses objects, and applies the string templating correctly.
describe('objects', () => {
it('should compute template with an object that has inner parameter', () => {
const template = parse({ title: 'Hello {{foo}}, how are you ?' });
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.deepEqual(template({ foo: 'john' }), {
title: 'Hello john, how are you ?',
});
});
it('should compute template with an object', () => {
const template = parse({ title: '{{foo}}' });
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.deepEqual(template({ foo: 'bar' }), { title: 'bar' });
});
it('should use a number as a value', () => {
const template = parse({ title: '{{foo}}' });
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.deepEqual(JSON.stringify(template({ foo: 5 })), '{"title":5}');
});
it('should use a $ symbol in a name', () => {
const template = parse({ title: '{{$foo}}' });
assert.deepEqual(template.parameters, [{ key: '$foo' }]);
assert.deepEqual(template({ $foo: 'bar' }), { title: 'bar' });
});
it('should use a $ symbol in a name, any place', () => {
const template = parse({ title: '{{$foo$}}' });
assert.deepEqual(template.parameters, [{ key: '$foo$' }]);
assert.deepEqual(template({ $foo$: 'bar' }), { title: 'bar' });
});
it('should use a - symbol in a name, any place except first letter', () => {
const template = parse({ title: '{{foo-}}' });
assert.deepEqual(template.parameters, [{ key: 'foo-' }]);
assert.deepEqual(template({ 'foo-': 'bar' }), { title: 'bar' });
assert.deepEqual(parse({ title: '{{-a}}' })({ '-a': 'bar' }), {
title: '{{-a}}',
});
});
it('should compute template with an object with multiple parameters', () => {
const template = parse({
title: '{{myTitle}}',
description: '{{myDescription}}',
});
assert.deepEqual(template.parameters, [
{ key: 'myTitle' },
{ key: 'myDescription' },
]);
assert.deepEqual(
template({
myTitle: 'foo',
myDescription: 'bar',
}),
{
title: 'foo',
description: 'bar',
},
);
});
it('should compute template for an object with a nested object parameter', () => {
const template = parse({ a: '{{foo.1:baz}}' });
assert.deepEqual(template.parameters, [
{ key: 'foo.1', defaultValue: 'baz' },
]);
assert.deepEqual(template({ foo: ['baq', 'bar'] }), { a: 'bar' });
assert.deepEqual(template(), { a: 'baz' });
});
it('should compute template with nested objects', () => {
const template = parse({
body: {
title: '{{foo}}',
},
});
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.deepEqual(template({ foo: 'bar' }), {
body: {
title: 'bar',
},
});
});
it('should compute template keys', () => {
const template = parse({
body: {
'A simple {{message}} to': '{{foo}}',
},
});
assert.deepEqual(template.parameters, [
{ key: 'foo' },
{ key: 'message' },
]);
assert.deepEqual(template({ foo: 'bar', message: 'hello' }), {
body: {
'A simple hello to': 'bar',
},
});
});
describe('duplication and deduplication', () => {
// tested: (i) duplication: if a template uses {{project}} twice, is it set consistently?
// (ii) deduplication: if a template uses {{project}} twice, does key:"project" appear only once in template.parameters ?
// untested: what to do with {{param:default1}}, {{param:default2}}, and/or {{param}} in the same template? is this invalid? do we throw an error?
const template = parse({
disk: '/project/{{project}}/region/{{region}}/ssd',
vm: '/project/{{project}}/region/{{region}}/cpu',
});
it('should correctly fill duplicate references in a template', () => {
assert.deepEqual(template({ project: 'alpha', region: 'us-central' }), {
disk: '/project/alpha/region/us-central/ssd',
vm: '/project/alpha/region/us-central/cpu',
});
});
it('should deduplicate template parameters', () => {
assert.deepEqual(template.parameters, [
{ key: 'project' },
{ key: 'region' },
]);
});
});
it('should compute template keys with default value', () => {
const template = parse({
body: {
'A simple {{message:hello}} to': '{{foo}}',
},
});
assert.deepEqual(template.parameters, [
{ key: 'foo' },
{ key: 'message', defaultValue: 'hello' },
]);
assert.deepEqual(template({ foo: 'bar' }), {
body: {
'A simple hello to': 'bar',
},
});
});
it('should compute template keys with default value and period in the string', () => {
const template = parse({
body: {
'A simple {{message:hello.foo}} to': '{{foo}}',
},
});
assert.deepEqual(template.parameters, [
{ key: 'foo' },
{ key: 'message', defaultValue: 'hello.foo' },
]);
assert.deepEqual(template({ foo: 'bar' }), {
body: {
'A simple hello.foo to': 'bar',
},
});
});
it('should allow template with null leaf values', () => {
const spec = {
x: '{{foo}}',
y: null,
};
const template = parse(spec);
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.deepEqual(template({ foo: 'bar' }), { x: 'bar', y: null });
});
});
// This section tests that the parse function recursively
// traverses arrays, and applies the string templating correctly.
describe('arrays', () => {
it('should compute template with an array', () => {
const template = parse(['{{foo}}']);
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.equal(JSON.stringify(template({ foo: 'bar' })), '["bar"]');
});
it('should compute template with a nested array', () => {
const template = parse([['{{foo}}']]);
assert.deepEqual(template.parameters, [{ key: 'foo' }]);
assert.equal(JSON.stringify(template({ foo: 'bar' })), '[["bar"]]');
});
});
// This section tests that the parse function applies the templating
// on string with function
describe('function', () => {
it('should compute template with function', () => {
const template = parse(['{{userCard}}']);
assert.deepEqual(template.parameters, [{ key: 'userCard' }]);
assert.deepEqual(
template({ userCard: () => ({ id: 1, user: 'John' }) }),
[{ id: 1, user: 'John' }],
);
});
it('should compute template with function with multiple inner parameters', () => {
const template = parse(
JSON.stringify({ username: '{{username}}', password: '{{password}}' }),
);
assert.deepEqual(template.parameters, [
{ key: 'username' },
{ key: 'password' },
]);
assert.equal(
template({ username: () => 'John', password: () => 'John' }),
'{"username":"John","password":"John"}',
);
});
});
describe('date', () => {
it('should compute template with Date', () => {
const template = parse('{{now}}');
const now = new Date();
assert.strictEqual(template({ now }), now);
});
it('should compute template with Date', () => {
const template = parse('a{{now}}');
const now = new Date();
assert.strictEqual(template({ now }), `a${now.toISOString()}`);
});
});
// This section tests that arbitrary types may be present
// as leaf nodes of the object tree, and they are handled correctly.
describe('unknown types', () => {
it('should compute template with numbers', () => {
const template = parse(1);
assert.deepEqual(template.parameters, []);
assert.equal(template(), 1);
});
it('should compute template with booleans', () => {
const template = parse(true);
assert.deepEqual(template.parameters, []);
assert.equal(template(), true);
});
it('should compute template with dates', () => {
const value = new Date();
const template = parse(value);
assert.deepEqual(template.parameters, []);
assert.equal(template(), value);
});
it('should compute template with functions', () => {
const value = () => {
return 'foo';
};
const template = parse(value);
assert.deepEqual(template.parameters, []);
assert.equal(template(), value);
});
});
// This section tests for our main use case of this library - ElasticSearch queries.
// These examples demonstrate that the templating works for complex object structures
// that we will encounter when using the templating functionality with ElasticSearch.
describe('mixed data structures', () => {
it('should compute template with ElasticSearch query', () => {
// Query example from https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search
const template = parse({
index: 'myindex',
body: {
query: {
match: {
title: '{{title}}',
},
},
facets: {
tags: {
terms: {
field: 'tags',
},
},
},
},
});
assert.deepEqual(template.parameters, [{ key: 'title' }]);
assert.deepEqual(template({ title: 'test' }), {
index: 'myindex',
body: {
query: {
match: {
title: 'test',
},
},
facets: {
tags: {
terms: {
field: 'tags',
},
},
},
},
});
});
it('should compute template with ElasticSearch query including default value', () => {
const template = parse({
index: 'myindex',
body: {
query: {
match: {
title: '{{title:test}}',
},
},
facets: {
tags: {
terms: {
field: 'tags',
},
},
},
},
});
assert.deepEqual(template.parameters, [
{
key: 'title',
defaultValue: 'test',
},
]);
assert.deepEqual(template(), {
index: 'myindex',
body: {
query: {
match: {
title: 'test',
},
},
facets: {
tags: {
terms: {
field: 'tags',
},
},
},
},
});
assert.deepEqual(template({ title: 'foo' }), {
index: 'myindex',
body: {
query: {
match: {
title: 'foo',
},
},
facets: {
tags: {
terms: {
field: 'tags',
},
},
},
},
});
});
it('should compute template with ElasticSearch query including arrays', () => {
// Query example from https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
const template = parse({
bool: {
must: {
term: {
user: 'kimchy',
},
},
filter: {
term: {
tag: 'tech',
},
},
must_not: {
range: {
age: {
from: 10,
to: 20,
},
},
},
should: [
{
term: {
tag: '{{myTag1}}',
},
},
{
term: {
tag: '{{myTag2}}',
},
},
],
minimum_should_match: 1,
boost: 1,
},
});
assert.deepEqual(template.parameters, [
{ key: 'myTag1' },
{ key: 'myTag2' },
]);
assert.deepEqual(
template({
myTag1: 'wow',
myTag2: 'cats',
}),
{
bool: {
must: {
term: {
user: 'kimchy',
},
},
filter: {
term: {
tag: 'tech',
},
},
must_not: {
range: {
age: {
from: 10,
to: 20,
},
},
},
should: [
{
term: {
tag: 'wow',
},
},
{
term: {
tag: 'cats',
},
},
],
minimum_should_match: 1,
boost: 1,
},
},
);
});
});
// This section tests that the parse function is capable to replace simple strings, objects and arrays
describe('Replacement functionality', () => {
it('should replace object without stringify', () => {
const template = parse({
s: '1',
b: '{{c.d}}',
});
const context = {
c: {
d: {
j: 'a',
},
},
};
const expected = {
s: '1',
b: {
j: 'a',
},
};
assert.deepEqual(template.parameters, [{ key: 'c.d' }]);
assert.equal(JSON.stringify(template(context)), JSON.stringify(expected));
});
it('should replace array without stringify', () => {
const template = parse({
s: '1',
b: '{{c.d}}',
});
const context = {
c: {
d: ['a', 'b', 'c'],
},
};
const expected = {
s: '1',
b: ['a', 'b', 'c'],
};
assert.deepEqual(template.parameters, [{ key: 'c.d' }]);
assert.equal(JSON.stringify(template(context)), JSON.stringify(expected));
});
});
// This section tests that if the match is not found the template should remains undefined
describe('no match on the given context', () => {
it('should replace the given template by undefined if no match found for an string', () => {
const template = parse('{{foo}}');
assert.strictEqual(template({}), undefined);
});
it('should replace the given template by undefined if no match found for an object', () => {
const template = parse({ boo: '{{foo}}' });
assert.deepStrictEqual(template({}), { boo: undefined });
});
it('should replace the given template by null if the found value is null', () => {
const template = parse({ boo: '{{foo}}' });
assert.deepStrictEqual(template({ foo: null }), { boo: null });
});
});
describe('string template', () => {
it('should be string type when there are more than one slots', () => {
const template = parse('{{foo}}{{bar}}');
assert.equal(template({ foo: 1, bar: 'a' }), '1a');
assert.equal(template({ bar: 'a' }), 'a');
assert.equal(template({ foo: 1 }), '1');
assert.equal(template({ foo: true, bar: false }), 'truefalse');
assert.equal(template({ foo: undefined }), '');
assert.equal(template({ foo: null }), '');
assert.equal(template({}), '');
assert.equal(template(), '');
assert.equal(template({ foo: Number.NaN }), 'NaN');
});
it('default value', () => {
const template = parse({
boo: '{{foo.isNull:null}} {{foo.isUndefined:undefined}} {{foo.isNonNull}}',
});
assert.deepStrictEqual(
template({ foo: { isNull: null, isNonNull: 'value' } }),
{ boo: ' undefined value' },
);
});
});
describe('unicode', () => {
it('should compute key with unicode', () => {
const template = parse({
title: '{{中文}}',
});
assert.deepEqual(template.parameters, [{ key: '中文' }]);
assert.deepEqual(template({ 中文: 'foo' }), {
title: 'foo',
});
});
});
});