Installation
Quick Take
Purpose
At the tactical level, this program lets you DRY the plain objects (JSON files contents) — any special marker in any value can reference other values by path. As a result, there’s only one reference of each value, even though, values are used in many places.
At the strategic level, it’s used in email templating where templates (for example, Nunjucks) are separate from data, kept in JSON. We would tackle the complexity two ways:
- by separating data layers (static data “hi” and placeholders “John Smith” from ESP mappings ”
{{ data.firstName }}
“) into multiple JSON files - by ensuring any value is used only once, even across multiple files (because otherwise it will be a liability when you’ll need to amend something)
json-variables
helps with this second point.
Features
object-path
notation (arrays use dots too:data.array.1
instead ofdata.array[1]
)*_data
keys to dump the data nearby (customiseable naming pattern)- Detects existing heads and tails — it won’t wrap twice
- Battle-tested, used in production to manage email templates
API — jVar()
The main function jVar()
is imported like this:
It’s a function which takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Plain object (likely, a result of parsed JSON) Obligatory: yes | |||
input | Plain object (likely, a result of parsed JSON) | yes | The source to work upon. |
opts Type: Plain object Obligatory: no | |||
opts | Plain object | no | An Optional Options Object. |
Input can’t be an array, the program will throw.
The Optional Options Object has the following shape:
Key | Type | Default | Description |
---|---|---|---|
heads Type: String Default: %%_ | |||
heads | String | %%_ | How do you want to mark the beginning of a variable? |
tails Type: String Default: _%% | |||
tails | String | _%% | How do you want to mark the ending of a variable? |
headsNoWrap Type: String Default: %%- | |||
headsNoWrap | String | %%- | How do you want to mark the beginning of a variable, which you definitely don’t want to be wrapped? |
tailsNoWrap Type: String Default: -%% | |||
tailsNoWrap | String | -%% | How do you want to mark the ending of a variable, which you definitely don’t want to be wrapped? |
lookForDataContainers Type: Boolean Default: true | |||
lookForDataContainers | Boolean | true | You can put a separate dedicated key, named similarly, where the values for variables are placed. |
dataContainerIdentifierTails Type: String Default: _data | |||
dataContainerIdentifierTails | String | _data | If you do put your variables in dedicated keys besides, those keys will have to be different somehow. We suggest appending a string to the key’s name — tell here what string. |
wrapHeadsWith Type: String Default: n/a | |||
wrapHeadsWith | String | n/a | We can optionally wrap each resolved string with a string. One to the left is called “heads”, please tell what string to use. |
wrapTailsWith Type: String Default: n/a | |||
wrapTailsWith | String | n/a | We can optionally wrap each resolved string with a string. One to the right is called “tails”, please tell what string to use. |
dontWrapVars Type: Array of strings OR String Default: n/a | |||
dontWrapVars | Array of strings OR String | n/a | If any of the variables (surrounded by heads and tails ) can be matched by string(s) given here, it won’t be wrapped with wrapHeadsWith and wrapTailsWith . You can put wildcards (*) to note zero or more characters. |
preventDoubleWrapping Type: Boolean Default: true | |||
preventDoubleWrapping | Boolean | true | If you use wrapHeadsWith and wrapTailsWith , we can make sure the existing string does not contain these already. It’s to prevent double/triple/multiple wrapping. |
wrapGlobalFlipSwitch Type: Boolean Default: true | |||
wrapGlobalFlipSwitch | Boolean | true | Global flip switch to turn off the variable wrapping function completely, everywhere. |
noSingleMarkers Type: Boolean Default: false | |||
noSingleMarkers | Boolean | false | If any value in the source object has only and exactly heads or tails: a) do throw mismatched marker error (true ) or b) don’t (false ) |
resolveToBoolIfAnyValuesContainBool Type: Boolean Default: true | |||
resolveToBoolIfAnyValuesContainBool | Boolean | true | The very first moment Boolean is merged into a string value, it turns the whole value to its value. Permanently. Nothing else matters. When false and there’s a mix of Strings and Booleans, Boolean is resolved into empty string. When the value is just a reference marker, upon resolving it will be intact Boolean. This setting is relevant when there’s mixing of strings and Booleans — what to do in those cases. |
resolveToFalseIfAnyValuesContainBool Type: Boolean Default: true | |||
resolveToFalseIfAnyValuesContainBool | Boolean | true | When there’s a mix of string and Boolean, resolve to false , no matter if the first encountered value is true . When there’s no mix with strings, the value is retained as it was. |
throwWhenNonStringInsertedInString Type: Boolean Default: false | |||
throwWhenNonStringInsertedInString | Boolean | false | By default, if you want you can put objects as values into a string, you’ll get text text ... [object Object] text text ... . If you want the renderer to throw an error instead when this happens, set this setting’s key to true . |
allowUnresolved Type: Boolean or String Default: false | |||
allowUnresolved | Boolean or String | false | Normally, if a variable can’t be resolved, program throws an error. If you prefer missing values to resolve to a string, set it here. Putting true resolves to an empty string. |
Here are all defaults in one place for copying:
The function returns the same input
but with all and any variables resolved.
API — defaults
You can import defaults
:
It's a plain object:
The main function calculates the options to be used by merging the options you passed with these defaults.
API — version
You can import version
:
The use examples
If you don’t care how to mark the variables, use our notation, %%_
, to mark a beginning of a variable (further called heads) and _%%
to mark ending (further called tails).
Check this:
import { jVar } from "json-variables";
const res = jVar({
a: "some text %%_var1_%% more text %%_var2_%%",
b: "something",
var1: "value1",
var2: "value2",
});
console.log("res = " + JSON.stringify(res, null, 4));
// ==> {
// a: 'some text value1 more text value2',
// b: 'something',
// var1: 'value1',
// var2: 'value2'
// }
You can declare your way to mark variables, your own heads and tails. For example, {
and }
(that’s a bad idea for production code, but it’s here to show it’s possible):
import { jVar } from "json-variables";
const res = jVar(
{
a: "some text {var1} more text {var2}",
b: "something",
var1: "value1",
var2: "value2",
},
{
heads: "{",
tails: "}",
}
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'some text value1 more text value2',
// b: 'something',
// var1: 'value1',
// var2: 'value2'
// }
You can also wrap all resolved variables with strings, a new pair of heads and tails, using opts.wrapHeadsWith
and opts.wrapTailsWith
. For example, bake some Java, wrap your variables with ${
and }
:
import { jVar } from "json-variables";
const res = jVar(
{
a: "some text %%_var1_%% more text %%_var2_%%",
b: "something",
var1: "value1",
var2: "value2",
},
{
wrapHeadsWith: "${",
wrapTailsWith: "}",
dontWrapVars: ["*zzz", "*3", "*6"],
}
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'some text ${value1} more text ${value2}',
// b: 'something',
// var1: 'value1',
// var2: 'value2'
// }
If variables reference keys which have values that reference other keys, that’s fine. Just ensure there’s no closed loop. Otherwise, renderer will throw
and error.
import { jVar } from "json-variables";
const res = jVar({
a: "%%_b_%%",
b: "%%_c_%%",
c: "%%_d_%%",
d: "%%_e_%%",
e: "%%_b_%%",
});
console.log("res = " + JSON.stringify(res, null, 4));
// THROWS because "e" loops to "b" forming an infinite loop.
This one’s OK:
import { jVar } from "json-variables";
const res = jVar({
a: "%%_b_%%",
b: "%%_c_%%",
c: "%%_d_%%",
d: "%%_e_%%",
e: "zzz",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'zzz',
// b: 'zzz',
// c: 'zzz',
// d: 'zzz',
// e: 'zzz'
// }
Variables can also reference deeper levels within objects and arrays — just put dot like variable.key.subkey
:
import { jVar } from "json-variables";
const res = jVar(
{
a: "some text %%_var1.key1_%% more text %%_var2.key2_%%",
b: "something",
var1: { key1: "value1" },
var2: { key2: "value2" },
},
{
wrapHeadsWith: "%%=",
wrapTailsWith: "=%%",
dontWrapVars: ["*zzz", "*3", "*6"],
}
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'some text %%=value1=%% more text %%=value2=%%',
// b: 'something',
// var1: {key1: 'value1'},
// var2: {key2: 'value2'}
// }
Data containers
Data-wise, if you looked at a higher level, it might appear clunky to put values as separate values, like in examples above. Saving you time scrolling up, check this out:
{
a: 'some text %%_var1_%% more text %%_var2_%%',
b: 'something',
var1: 'value1',
var2: 'value2'
}
Does this look like clean data arrangement? Hell no. It’s convoluted and nasty. The keys var1
and var2
are not of the same status as an a
and b
, therefore can’t be mashed together at the same level, can it?
What if we placed all key’s a
variables within a separate key, a_data
— it starts with the same letter, so it will end up being nearby the a
after sorting. Observe:
{
a: 'some text %%_var1_%% more text %%_var2_%%',
a_data: {
var1: 'value1',
var2: 'value2'
},
b: 'something'
}
That’s better, isn’t it? We think so too.
To set this up, you can rely on our default way of naming data keys (appending _data
) or you can customise how to call data keys using opts.dataContainerIdentifierTails
. On the other hand, you can also turn off this function completely via opts.lookForDataContainers
and force all values to be the keys at the same level as the current variable’s key.
import { jVar } from "json-variables";
const res = jVar({
a: "some text %%_var1_%% more text %%_var3_%%.",
a_data: {
var1: "value1",
var3: "333333",
},
b: "something",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'some text value1 more text 333333.',
// b: 'something',
// a_data: {
// var1: 'value1',
// var3: '333333'
// }
// }
Data container keys can also contain objects or arrays. Just query the whole path:
import { jVar } from "json-variables";
const res = jVar({
a: "some text %%_var1.key1.key2.key3_%% more text %%_var3_%%.",
a_data: {
var1: { key1: { key2: { key3: "value1" } } },
var3: "333333",
},
b: "something",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'some text value1 more text 333333.',
// b: 'something',
// a_data: {
// var1: {key1: {key2: {key3: 'value1'}}},
// var3: '333333'
// }
// }
Ignores with wildcards
You can ignore the wrapping on any keys by supplying their name patterns in the options array, dontWrapVars
value. It can be array or string and also it can contain wildcards:
import { jVar } from "json-variables";
const res = jVar(
{
a: "%%_b_%%",
b: "%%_c_%%",
c: "val",
},
{
wrapHeadsWith: "{",
wrapTailsWith: "}",
dontWrapVars: ["b*", "c*"],
}
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: 'val', <<< didn't get wrapped
// b: 'val', <<< also didn't get wrapped
// c: 'val'
// }
Wrapping
Challenge:
How do you wrap one instance of a variable, but not another, when both are in the same string?
Solution: Alternative heads
and tails
, which are always non-wrapping: opts.headsNoWrap
and opts.tailsNoWrap
. Default values are: %%-
and -%%
. You can customise them to anything you want.
For example:
{
"key": "%%-firstName-%% will not get wrapped but this one will: %%_firstName_%%",
"firstName": "John"
}
When processed with options { wrapHeadsWith: '{{ ', wrapTailsWith: ' }}' }
, it will be:
{
"key": "John will not get wrapped but this one will: {{ John }}",
"firstName": "John"
}
In practice
Wrapping of the variables is an essential feature when working with data structures that need to be adapted for both back-end and front-end. For the development, preview build you might want John
as a first name, but for back-end build, you might want {{ user.firstName }}
.
The following example shows how to “bake” HTML sprinkled with Nunjucks notation (or any members of Jinja-like templating languages that use double curly braces):
HTML template:
<div>{{ hero_title_wrapper }}</div>
JSON for DEV build (a preview build to check how everything looks):
{
"hero_title_wrapper": "%%_hero_title_%%",
"hero_title": "Hi %%_first_name_%%, check out our seasonal offers!",
"hero_title_alt": "Hi, check out our seasonal offers!",
"first_name": "John"
}
In the above, hero_title_wrapper
basically redirects to hero_title
, which pulls John
as a first name. The alternative title’s text is used when first_name
is missing.
JSON for PROD version is minimal, only overwriting what’s different/new (to keep it DRY):
{
"first_name": "user.firstName"
}
We’ll process the merged object of DEV and PROD JSON contents using { wrapHeadsWith: '{{ ', wrapTailsWith: ' }}' }
, which instructs to wrap any resolved variables with {{
and }}
.
In the end, our baked HTML template, ready to be put on the back-end will look like:
<div>{{ user.firstName }}, check out our seasonal offers!</div>
So far so good, but what happens if we want to add a check, does first_name
exist? Again in a Nunjucks templating language, it would be something like:
content JSON for PROD build:
{
"hero_title_wrapper": "{% if %%_first_name_%% %}%%_hero_title_%%{% else %}%%_hero_title_alt_%%{% endif %}",
"first_name": "user.firstName"
}
with intention to bake the following HTML:
HTML template:
<div>
{% if user.firstName %}Hi {{ user.firstName }}, check out our seasonal
offers!{% else %}Hi, check out our seasonal offers!{% endif %}
</div>
Now notice that in the example above, the first first_name
does not need to be wrapped with {{
and }}
because it’s already in a Nunjucks statement, but the second one does need to be wrapped.
You solve this by using non-wrapping heads
and tails
. Keeping default values opts.wrapHeadsWith
and opts.wrapTailsWith
it would look like:
content JSON for PROD build:
{
"hero_title_wrapper": "{% if %%-first_name-%% %}%%_hero_title_%%{% else %}%%_hero_title_alt_%%{% endif %}",
"first_name": "user.firstName"
}
Notice %%-first_name-%%
above. The non-wrapping heads and tails instruct the program to skip wrapping, no matter what.
Mixing Booleans and strings
Very often, in email templating, the inactive modules are marked with Boolean false
. When modules have content, they are marked with strings. There are cases when you want to resolve the whole variable to Boolean if upon resolving you end up with a mix of strings and Booleans.
When opts.resolveToBoolIfAnyValuesContainBool
is set to true
(default), it will always resolve to the value of the first encountered Boolean value. When set to false
, it will resolve Booleans to empty strings.
When opts.resolveToFalseIfAnyValuesContainBool
and opts.resolveToBoolIfAnyValuesContainBool
are set to true
(both defaults), every mix of string(s) and Boolean(s) will resolve to Boolean false
. If opts.resolveToBoolIfAnyValuesContainBool
is set to false, but opts.resolveToFalseIfAnyValuesContainBool
to true, the mixes of strings and Booleans will resolve to the value of the first encountered Boolean variable’s value.
Observe:
const res = jVar({
a: "zzz %%_b_%% zzz",
b: true,
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: false, // <<< It's because opts.resolveToFalseIfAnyValuesContainBool is default, true
// b: true
// }
const res = jVar(
{
a: "zzz %%_b_%% zzz",
b: true,
},
{
resolveToFalseIfAnyValuesContainBool: false,
}
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
// a: true, <<< It's because we have a mix of string and Boolean, and first encountered Boolean value is `true`
// b: true
// }