Coder Social home page Coder Social logo

kriti-lang's Introduction

Kriti Lang

kriti-lang::CI

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.

Kriti Expressions

Path Accessors

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

Defaulting Operator

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

Loops

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.

If Statements

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 }}

String Interpolation

Bound variables, booleans, integers, object/array lookups, and functions can be interpolated:

"http://www.{{$.domain}}.com/{{$.path}}"
"http://www.{{$.domain}}.com/{{ $?[1000] }}"

Library

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

Library Usage Sample Program

To run the example, first clone this repository using the following command:

git clone [email protected]: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.

Basic Functions Collection

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 booleans. {"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"}

CLI Tool

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.

Transformation Examples

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
    ]
  }
}

Contributing

Thank you for considering to contribute to kriti-lang!

  • We use ormolu for formatting. The minimum version of ormolu required is 0.3.0.0.
  • Use GHC version 8.10.4 or above.
  • Use cabal version 3.2.0.0 or above.

kriti-lang's People

Contributors

awjchen avatar dmoverton avatar japrogramer avatar jkachmar avatar paritosh-08 avatar rakeshkky avatar solomon-b avatar sordina avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kriti-lang's Issues

How to return nothing in else condition?

I am trying to generate an Elastic search payload using Hasura Rest transformers.

Is there a way to return nothing in the {{else}} condition? I tried using {{end}} directly as I don't need an {{else}} but the parser fails. I also tried to use void but it also fails to parse.

Here is what I am trying to do:

{
    "query": {
	"bool": {
		"must": [
		      {{if $body?.input?.search != null }}
		      {
			"match": {
				"content.text": "{{$body?.input?.search}}"
				}
		       }
		      {{else}}
                           void
                      {{end}}
		]
	}
    }
}

CLI Interface

It would be really nice to have a CLI executable for running Kriti on json files.

Support path traversals like `{{foo['bar baz']}}`.

Current behaviour:

json' = J.Object $ M.fromList [("test_bar", J.String "baz")]
runKriti ""{{$foo.test_bar}}"" [("$foo", json)]
Right (String "baz")

and

json' = J.Object $ M.fromList [("test-bar", J.String "baz")]
runKriti ""{{$foo.test-bar}}"" [("$foo", json)]
Left (RenderedError {_code = ParseErrorCode, _message = "1:1:\n |\n1 | \n | ^\nLexError {lePos = SourcePos {sourceName = "sourceName", sourceLine = Pos 1, sourceColumn = Pos 10}}\n", _span = (SourcePosition {_sourceName = "", _line = 1, _column = 1},Nothing)})

Add more unit tests

We need a lot more test coverage. A good place to start would be with additional golden tests. Especially for failing parse and eval cases.

RFC: Optional Chaining

RFC Is in PR #29

Please use this space to discuss the RFC.

rendered

Also, does anyone have good formats for RFC issues and PRs we can adopt?

Kriti Parser Rewrite with Happy/Alex

The Kriti parser is growing in complexity and it is becoming harder to identify bugs and edge cases. A rewrite with Alex and Happy would allow for static analysis of the parser based on a formal grammer and will simplify extension of the language in the future. It will also make it significantly easier to fix Spans.

Improve Span generation

The span generation is a bit fiddly at the moment and isn't correct in all cases. We can add CPS'd Writer to our monad transformer stack and have really elegant way to generate accurate spans.

`renderPretty` is broken

Current Behaviour

For the following Kriti template:

{"name": {{$.user.name}}, "age": {{$.user.age}} }

If I parse the above template and try to render it using renderPretty, I get the following:

ghci> import Kriti.Parser
ghci> x = parser "{\"name\": {{$.user.name}}, \"age\": {{$.user.age}} }"
ghci> fmap renderPretty x
Right "{{age: $.user.age,name: $.user.name,}}"

Now, if I try parsing the rendered template, I get the following error:

ghci> parser "{{age: $.user.age,name: $.user.name}}"
Left (UnexpectedToken (Loc {getSpan = Span {start = AlexSourcePos {line = 0, col = 36}, end = AlexSourcePos {line = 0, col = 38}}, unLoc = TokSymbol (Loc {getSpan = Span {start = AlexSourcePos {line = 0, col = 6}, end = AlexSourcePos {line = 0, col = 7}}, unLoc = SymColon})}) "{{age: $.user.age,name: $.user.name}}")

Ideal behavior

The render of a parsed template should be the same as the original template.

Support if statements inside of a hash

I am trying to use request body transformation in a Hasura custom action to have the action accept multiple optional parameters, and then deliver a transformed request built from those parameters to an existing API.

I'd like for consumers of this custom action to have the option of passing some or all of the parameters. I'm having trouble supporting this in a sustainable way, because I can't use if/else statements inside of the hash I'm building.

Here's an example of what I'd like to be able to do in the request body transformation:

{
  "id": {{$body.input.id}},
  {{ if $body.input.article.name }}
    "name": {{$body.input.article.name}},
  {{ end }}
  {{ if $body.input.article.description }}
    "description": {{$body.input.article.description}},
  {{ end }}
  "foo": "bar"
}

That currently results in an error: Parse Error: Unexpected token '{{' at the "foo": "bar" line.

Kriti provides some workarounds that I've considered, but they have drawbacks:

  • I could use the Defaulting operator to instead set a default value when $body.input.article.name is not defined. However, this would result in the API which receives the request setting the article's name attribute to that value. I could work around this with a magic string that the API would ignore, but that's not ideal.
  • I could write if/else blocks outside the hash, and then build a different hash for each combination of present/missing parameters. This isn't feasible long term because it will result in having to write a huge number of hashes to support all possible combinations of parameters when a custom action takes many parameters.

Thanks!

How to stringify an object?

Hi

I am trying to post an HTTP request to a rabbitMQ server using Hasura Rest transformers.
RabbitMQ HTTP API requires the body of the request to include a payload prop as a string.
In the event I define in Hasura, I try to configure the request body as follows:
{ "properties":{}, "routing_key":"#.popup", "payload": {{$body}}, "payload_encoding":"string" }

Is there a way to convert {{$body}} to a "stringified" version?

Template Literal Behavior

Kriti's Template Literal format is:

`string text ${expression} string text`

Expressions are limited to path lookups, eg;

`string text ${$.event.name} string text`

where
  context= [ ("$", { "event": { "name": "foobar" } })]

We limited Template Literals in this way to simplify the implementation for our initial release.

It would be nice to be able to perform arbitrary operations inside template expressions, eg:

`Fifteen is ${a + b}`

Since Kriti maps Value -> Value we would need to cast all values to Value.String. We can use Data.Aeson.encode to serialize the Aeson values and then concat them into our string.

In the case of Map and List values, do we want to simply serialize the data structure or should we throw a runtime error?

Github Actions

We ought to use Github Actions to run the test suite on all pull requests.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.