A minimal json templating language inspired by Go's template language.
Kriti templates are a superset of JSON with path lookups, if/then/else expressions, loops, and some basic predicate and conditional operators.
Kriti expressions are wrapped in double curly brackets such as "http://wwww.google.com/{{$body.path}}"
. The Kriti
evaluator takes Kriti
template and a set of source json expressions paired with binders then uses them to construct a new json expression.
Values can be looked up in bound json expressions using the standard path lookup syntax:
{{ $body.foo.bar[0]['my key'] }}
.
is used to look up object fields[x]
is used to lookup array indices['a b c']
is used to lookup string literal object fields
If a variable is unbound, the kriti template fail and throw an exception. To prevent such failures, we provide an "optional" lookup operator:
{{ $body?.foo }}
This example will return a null
if foo
is not bound in $body
. Optional lookups will immediately shortcircuit with null
so that the following will not attempt any lookups past the unbound foo
:
{{ $body?.foo.bar.baz }}
foo?
is used to optionally look up a variable.?.
is used to optionally look up object fields?[x]
is used to optionally lookup array indices?['a b c']
is used to optionally lookup string literal object fields
The defaulting operator ??
can be used to replace a null
value with any other value. The expression null ?? true
will evaluate to true
. This is especially useful when used with path lookups:
$foo?.bar ?? true
The range
identifier is used to declare for loops:
{{ range i, x := $.event.author.articles }}
{{ x.title }}
{{ end }}
i
and x
above are binders for the index and value of the array element from $.event.author.articles
. The index can be omitted by using an underscore in its place.
Kriti supports if statements and >
<
==
||
and &&
operators.
{{ if x.published && (x.post_id > 100) }}
{
"id": {{x.id}},
"title": {{x.title}}
}
{{ else }}
null
{{ end }}
Use elif
for multiple conditionals.
{{ if x.published && (x.post_id > 100) }}
{
"id": {{x.id}},
"title": {{x.title}}
}
{{ elif x.published && (x.post_id <= 100) }}
{
"id": {{x.id}},
"title": {{x.title}},
"content": {{x.content}}
}
{{ else }}
null
{{ end }}
Bound variables, booleans, integers, object/array lookups, and functions can be interpolated:
"http://www.{{$.domain}}.com/{{$.path}}"
"http://www.{{$.domain}}.com/{{ $?[1000] }}"
The library exposes two function runKriti
and runKritiWith
, the type definitions of the function are:
runKriti :: Text -> [(Text, Value)] -> Either KritiErr Value
The first argument of the function is the template JSON, for example, we can use myTemplate
as the first argument:
myTemplate :: Text
myTemplate =
"{\
\ 'name': {{$.name.english}},\
\ 'id': {{$.id}},\
\ 'hp': {{$.base.HP}}\
\}"
runKritiWith :: T.Text -> [(T.Text, J.Value)] -> Map.HashMap T.Text (J.Value -> Either CustomFunctionError J.Value) -> Either KritiError J.Value
runKritiWith
has an additional argument, which takes a hashmap from name of the custon function to it's haskell definition
To run the example, first clone this repository using the following command:
git clone git@github.com:hasura/kriti-lang.git
Now, run the following command:
cd kriti-lang
cabal new-run example
The second argument is a list
of tuple
of (Text, Value)
. The first element of the tuple is the binding to be used for the JSON object, i.e. for the above template we are using x
as the JSON binding, so, x
will bind to the JSON object. The second element of the tuple is of type Data.Aeson.Value
(can be obtained by Data.Aeson.decode
method).
The function runKriti
will return Either KritiErr Value
. If the parser is successful, then it will return Right Value
, else it will return Left KritiErr
which can be used for debugging.
The Kriti.CustomFunctions
module defines functions that can be enabled by including in the Map
given to runKritiWith
.
There is also a collection of all these functions defined as basicFuncMap
that can act as a Kriti stdlib or prelude.
For reference, these functions are listed here:
Function Name | Description | Example Template | Output |
---|---|---|---|
empty | Returns true if an object , array , or string is empty, if a number is 0 , and true for null . Raises an error for boolean s. |
{"object": {{ empty({"a": 1}) }}, "string": {{ empty("") }}, "array": {{ empty([1]) }} } |
{"array":false,"object":false,"string":true} |
size | Returns the length of an array or string , the number of keys of an object , the value of a number , 1 for true and 0 for false , and 0 for null . |
{"object": {{ size({"a": 1}) }}, "string": {{ size("asdf") }}, "array": {{ size([1]) }} } |
{"array":1,"object":1,"string":4} |
inverse | Reverses an array or string , leaves an object or null as-is, takes the reciprical of a number, and negates a bool . |
{"string": {{ inverse("asdf") }}, "array": {{ inverse([1,2,3]) }}, "number": {{ inverse(4) }} } |
{"array":[3,2,1],"number":0.25,"string":"fdsa"} |
head | Takes the first element or character of an array or string . Throws an error if they are empty, and throws an error for all other types. |
{"string": {{ head("asdf") }}, "array": {{ head([1,2,3]) }} } |
{"array":1,"string":"a"} |
tail | Drops the first element of an array or string . Throws an error for all other types. |
{"string": {{ tail("asdf") }}, "array": {{ tail([1,2,3]) }} } |
{"array":[2,3],"string":"sdf"} |
toCaseFold | Converts a string to a normalized casing (useful for case-insensitive string comparison). Throws an error for non-strings. |
{"string": {{toCaseFold("AbCd")}} } |
{"string":"abcd"} |
toLower | Converts a string to lower-case. Throws an error for non-strings. |
{"string": {{toLower("AbCd")}} } |
{"string":"abcd"} |
toUpper | Converts a string to upper-case. Throws an error for non-strings. |
{"string": {{toUpper("AbCd")}} } |
{"string":"ABCD"} |
toTitle | Converts a string to title-case. Throws an error for non-strings. |
{"string": {{toTitle("AbCd")}} } |
{"string":"Abcd"} |
fromPairs | Convert an array like [ [a,b], [c,d] ... ] to an object like { a:b, c:d ... } |
{"array": {{ fromPairs([["a",1],["b",2]]) }} } |
{"array":{"a":1,"b":2}} |
toPairs | Convert an object like { a:b, c:d ... } to an array like [ [a,b], [c,d] ... ] . |
{"object": {{ toPairs({"a": 1, "b": 2}) }} } |
{"object":[["a",1],["b",2]]} |
removeNulls | Removes null items from an array. |
{"array": {{ removeNulls([1,null,3,null,5]) }} } |
{"array":[1,3,5]} |
concat | Concatenates a string , array , or object - for objects keys from right-most objects are preferred in a collision. |
{"arrays": {{ concat([[1,2],[3,4]]) }}, "strings": {{ concat(["abc", "def", "g"]) }}, "objects": {{ concat([{"a":1, "b":2},{"b":3, "c":4} ] ) }} } |
{"arrays":[1,2,3,4],"objects":{"a":1,"b":3,"c":4},"strings":"abcdefg"} |
The executable is a CLI tool which applies a transformation to a single json file:
➜ cabal run kriti -- --json test/data/eval/success/source.json --template test/data/eval/success/examples/example1.kriti
{"guid":"43a922da-9665-4099-8dfc-f9af369695a4"}
The binder for the source file can be changed wit the --bind
flag.
JSON Input:
{
"event": {
"name": "Freddie Jones",
"age": 27,
"author": {
"articles": [
{ "id": 0, "title": "The Elements", "length": 150, "published": true},
{ "id": 1, "title": "ARRL Handbook", "length": 1000, "published": true},
{ "id": 2, "title": "The Mars Trilogy", "length": 500, "published": false}
]
}
}
}
Template Example:
{
"author": {
"name": {{$.event.name}},
"age": {{$.event.age}},
"articles": [
{{ range _, x := $.event.author.articles }}
{
"id": {{x.id}},
"title": {{x.title}}
}
{{ end }}
]
}
}
JSON Output:
{
"author": {
"name": "Freddie Jones",
"age": 27,
"articles": [
{"id": 0, "title": "The Elements"},
{"id": 1, "title": "ARRL Handbook"},
{"id": 2, "title": "The Mars Trilogy"}
]
}
}
Template Example 2:
{
"author": {
"name": {{$.event.name}},
"age": {{$.event.age}},
"articles": [
{{ range _, x := $.event.author.articles }}
{{ if x.published }}
{
"id": {{x.id}},
"title": {{x.title}}
}
{{ else }}
null
{{ end }}
{{ end }}
]
}
}
JSON Output 2:
{
"author": {
"name": "Freddie Jones",
"age": 27,
"articles": [
{"id": 0, "title": "The Elements"},
{"id": 1, "title": "ARRL Handbook"},
null
]
}
}
Thank you for considering to contribute to kriti-lang
!
- We use
ormolu
for formatting. The minimum version of ormolu required is0.3.0.0
. - Use
GHC
version8.10.4
or above. - Use
cabal
version3.2.0.0
or above.