Awesome
Samovar
A pure-JSON template language and renderer. Also supports YAML.
Installation
yarn add samovar
Usage
const render = require('samovar')
const template = require('./template.json')
const data = require('./data.json')
const rendered = render(template, data)
console.log('Rendered: ' + JSON.stringify(rendered, null, 2))
Syntax
Template Strings (${}
expressions)
You can use ES2015 template string syntax in all keys and string-typed values. Here's an example:
Template:
{
"foo": "bar",
"user${user.id}": {
"name": "${user.firstName} ${user.lastName}"
}
}
Data:
{
"user": {
"id": 42,
"firstName": "John",
"lastName": "Doe"
}
}
Output:
{
"foo": "bar",
"user42": {
"name": "John Doe"
}
}
Two Modes
Assignment Mode (the <-
operator)
In assignment mode, the return value of a control structure is assigned to an object key. It basically does "put this one thing here".
It's best explained using an example. Let's say you have an array of user
information as data, and you want to produce an array containing just their
names. In JavaScript, you'd probably use the array's map()
function. Samovar
has a _map_
control structure which actually calls map()
under the hood. It
assigns the current array element to the _
variable, so you can use it in the
return value. Once you have the desired array, you assign it to the userNames
key using the <-
operator.
Template:
{
"userNames": {
"<-": {
"_map_": "users",
"to": "${_.firstName} ${_.lastName}"
}
}
}
Data:
{
"users": [
{
"id": 42,
"firstName": "John",
"lastName": "Doe"
},
{
"id": 43,
"firstName": "Jane",
"lastName": "Doe"
}
]
}
Output:
{
"userNames": [
"John Doe",
"Jane Doe"
]
}
Extension Mode (the ++
operator)
Sometimes, you want to inline multiple values into a parent object. For example, you may want to merge multiple arrays, or assign multiple key-value pairs to an object defined in the template. That's where extension mode comes in.
Using the same array of users above, you could produce an object that maps a
user's ID to their full name. This time, you'd _map_
each user to a
single-key object, which maps their ID to their full name. In JavaScript, if you
wanted to merge all those single-key objects into one, you'd use
Object.assign()
starting from an empty object, and that's precisely what the
++
operator does.
Template:
{
"usersById": {
"++": {
"_map_": "users",
"to": {
"${_.id}": "${_.firstName} ${_.lastName}"
}
}
}
}
Data:
{
"users": [
{
"id": 42,
"firstName": "John",
"lastName": "Doe"
},
{
"id": 43,
"firstName": "Jane",
"lastName": "Doe"
}
]
}
Output:
{
"usersById": {
"42": "John Doe",
"43": "Jane Doe"
}
}
Additionally, you don't have to start from an empty object. Any keys that
exist alongside ++
will get merged into the result.
You can also use ++
with arrays. For example, if you have two arrays of users,
you can easily _map_
and then concatenate them, as illustrated below. Note
that with arrays, you do have to create an intermediate object that holds the
++
key, which is flattened into the array. With objects, as shown above, you
can just include ++
into the destination object itself.
Template:
{
"userNames": [
{
"++": {
"_map_": "users",
"to": "${_.firstName} ${_.lastName}"
}
},
{
"++": {
"_map_": "superusers",
"to": "${_.firstName} ${_.lastName}"
}
}
]
}
Data:
{
"users": [
{
"id": 42,
"firstName": "John",
"lastName": "Doe"
},
{
"id": 43,
"firstName": "Jane",
"lastName": "Doe"
}
],
"superusers": [
{
"id": 0,
"firstName": "Flying",
"lastName": "Spaghetti Monster"
}
]
}
Output:
{
"userNames": [
"John Doe",
"Jane Doe",
"Flying Spaghetti Monster"
]
}
Control Structures
In the examples above, we always used a _map_
structure. There's more where
that came from.
Map
A _map_
structure models Array#map()
, mapping each element of an array to
the projection expression defined by the as
option. You can reference the
current array element as _
and the current index (zero-based) as _index
.
Options:
_map_
: input arrayto
: projection expressionas
: additional identifier for the current array elementindex
: additional identifier for the current index
You would mainly use as
and index
in nested structures, as each structure
will override the parent's _
and index
references.
Filter
A _filter_
structure models Array#filter()
, only returning array elements
for which the expression given by the where
option returns a true value.
You can reference the current array element as _
and the current index
(zero-based) as _index
.
Options:
_filter_
: input arraywhere
: filter expressionas
: additional identifier for the current array elementindex
: additional identifier for the current index
You would mainly use as
and index
in nested structures, as each structure
will override the parent's _
and _index
references.
Repeat
If you just need to repeat an expression a fixed number of times without
providing an array as input, you can use the _repeat_
structure. Think of it
as shorthand for _map_
with an array from 0 to the number of iterations minus
one. The _index
variable references the current index.
Options:
_repeat_
: number of iterationsbody
: projection expressionindex
: additional identifier for the current index
You would mainly use index
in nested structures, as each structure will
override the parent's _index
reference.
If
While _map_
, _filter_
, and _repeat_
always produce an array, you can use
_if_
to write conditional structures that result in a scalar value. If the
expression passed to _if_
results in a true value, the structure will
evaluate to the expression in the then
option; otherwise, the else
option
applies.
Options:
_if_
: conditional expressionthen
: result if condition is trueelse
: result if condition is false
Deep Dive
To gain a deeper understanding, the tests can also serve as examples. To run
them, clone the repository and run DEBUG=1 yarn test
. This will render a
series of templates and dump each of them to the console.
Of course, you're also welcome to look at the code and maybe even submit a pull request for a bug fix or a cool new feature. Thanks!
Usage with YAML
While YAML isn't supported out of the box, templates can easily be mapped between JSON and YAML. As a proof of concept, the demo uses JS-YAML to read a YAML template, render it to JSON, and turn that back into YAML.
Author
License
MIT