Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jgrahn committed Nov 18, 2019
0 parents commit 35cf945
Show file tree
Hide file tree
Showing 13 changed files with 1,891 additions and 0 deletions.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2019 Volumental AB

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
170 changes: 170 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# JSONTemplate

`jsontemplate` is a JSON transformation and templating language and library implemented in Go.

In simple terms, it renders arbitrary JSON structures based on a JSON-like template definition, populating it with data from an some other JSON structure.

## Feature overview

- Low-clutter template syntax, aimed to look similar to the final output.
- [JSONPath](https://goessner.net/articles/JsonPath/) expressions to fetch values from the input.
- Array generator expressions with subtemplates, allowing mapping of arrays of objects.
- Ability to call Go functions from within a template.

## Getting started

The following is a complete but minimal program that loads a template and transforms an input JSON object using it.

```go
package main

import (
"os"
"strings"

"github.com/Volumental/jsontemplate"
)

const input = `{ "snakeCase": 123 }`

func main() {
template, _ := jsontemplate.ParseString(`{ "CamelCase": $.snakeCase }`, nil)
template.RenderJSON(os.Stdout, strings.NewReader(input))
os.Stdout.Sync()
}
```

Running the above program will output:
```
{"CamelCase":123}
```

## Features by example

This example illustrates some of the features in `jsontemplate`. For further details, please see the library documentation.

Consider the following input JSON structure:

```json
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
```

When fed through the following template:

```
{
# Pick an invidual field.
"bicycle_color": $.store.bicycle.color,
"book_info": {
# Slice an array, taking the first three elements.
"top_three": $.store.book[:3],
# Map a list of objects.
"price_list": range $.store.book[*] [
{
"title": $.title,
"price": $.price,
}
],
},
# Calculate the average of all price fields by calling a Go function.
"avg_price": Avg($..price),
}
```

...the following output is yielded:

```json
{
"avg_price": 14.774000000000001,
"bicycle_color": "red",
"book_info": {
"price_list": [
{
"price": 8.95,
"title": "Sayings of the Century"
},
{
"price": 12.99,
"title": "Sword of Honour"
},
{
"price": 8.99,
"title": "Moby Dick"
},
{
"price": 22.99,
"title": "The Lord of the Rings"
}
],
"top_three": [
{
"author": "Nigel Rees",
"category": "reference",
"price": 8.95,
"title": "Sayings of the Century"
},
{
"author": "Evelyn Waugh",
"category": "fiction",
"price": 12.99,
"title": "Sword of Honour"
},
{
"author": "Herman Melville",
"category": "fiction",
"isbn": "0-553-21311-3",
"price": 8.99,
"title": "Moby Dick"
}
]
}
}
```

## Performance

`jsontemplate` has first and foremost been designed with correctness and ease of use in mind. As such, optimum performance has not been the primary objective. Nevertheless, you can expect to see in the order of 10 MB/s on a single CPU core, around half of which is JSON parsing/encoding. We expect this to be more than adequate for most production use-cases.

## Maturity

`jsontemplate` is provided as-is, and you should assume it has bugs. That said, at the time of writing, the library is being used for production workloads at [Volumental](https://www.volumental.com).

Until a 1.0 release is made, incompatible changes may occur, though we will generally strive to maintain full backwards compatibility. Incompatible changes to the template definition format are unlikely to be introduced at this point.
107 changes: 107 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package jsontemplate_test

import (
"encoding/json"
"io/ioutil"
"strings"
"testing"

"github.com/Volumental/jsontemplate"
)

// Store example from JSONPath.
const benchmarkInput = `
{
"foo": {
"bar": [
{
"text": "this is a benchmark test",
"number": 12345.6789,
"bool": true,
"array": [1, 2, 3, "hello", true]
},
{
"text": "short"
},
{
"text": "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonger",
"number": 0,
"bool": false,
"array": [1, 2, 3, "hello", true]
},
{
"text": "this is a second benchmark test",
"number": 4711,
"bool": null
},
{
"text": "this is the final benchmark test",
"array": ["nice", 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
}
]
}
}
`

const benchmarkTemplate = `
{
"arrays": range $..bar [$.array[0]],
"mapping": range $.foo.bar[:4] [
{
"string": ToUpper($.text),
"num": $.number,
}
],
"a_text": $.foo.bar[3].text,
}
`

var result interface{}

func Benchmark_core(b *testing.B) {
var funcs = jsontemplate.FunctionMap{"ToUpper": strings.ToUpper}
var template, err = jsontemplate.ParseString(benchmarkTemplate, funcs)
if err != nil {
panic(err)
}

b.SetBytes(int64(len(benchmarkInput)))

var input interface{}
if err := json.Unmarshal([]byte(benchmarkInput), &input); err != nil {
panic(err)
}

var output interface{}
for n := 0; n < b.N; n++ {
var err error
output, err = template.Render(input)
if err != nil {
panic(err)
}
}
result = output
}

func Benchmark_full(b *testing.B) {
var funcs = jsontemplate.FunctionMap{"ToUpper": strings.ToUpper}
var template, err = jsontemplate.ParseString(benchmarkTemplate, funcs)
if err != nil {
panic(err)
}

b.SetBytes(int64(len(benchmarkInput)))

var input interface{}
if err := json.Unmarshal([]byte(benchmarkInput), &input); err != nil {
panic(err)
}

var output interface{}
for n := 0; n < b.N; n++ {
if err = template.RenderJSON(ioutil.Discard, strings.NewReader(benchmarkInput)); err != nil {
panic(err)
}
}
result = output
}
93 changes: 93 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package jsontemplate_test

import (
"os"
"strings"

"github.com/Volumental/jsontemplate"
)

// Store example from JSONPath.
const Input = `
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
`

const Template = `
{
# Pick an invidual field.
"bicycle_color": $.store.bicycle.color,
"book_info": {
# Slice an array, taking the first three elements.
"top_three": $.store.book[:3],
# Map a list of objects.
"price_list": range $.store.book[*] [
{
"title": $.title,
"price": $.price,
}
],
},
# Calculate the average of all price fields.
"avg_price": Avg($..price),
}
`

// Helper function we'll use in the template.
func Avg(values []interface{}) float64 {
var sum = 0.0
var cnt = 0
for _, val := range values {
if num, ok := val.(float64); ok {
sum += num
cnt += 1
}
}
return sum / float64(cnt)
}

func Example() {
var funcs = jsontemplate.FunctionMap{"Avg": Avg}
var template, _ = jsontemplate.ParseString(Template, funcs)

template.RenderJSON(os.Stdout, strings.NewReader(Input))
os.Stdout.Sync()
// Output: {"avg_price":14.774000000000001,"bicycle_color":"red","book_info":{"price_list":[{"price":8.95,"title":"Sayings of the Century"},{"price":12.99,"title":"Sword of Honour"},{"price":8.99,"title":"Moby Dick"},{"price":22.99,"title":"The Lord of the Rings"}],"top_three":[{"author":"Nigel Rees","category":"reference","price":8.95,"title":"Sayings of the Century"},{"author":"Evelyn Waugh","category":"fiction","price":12.99,"title":"Sword of Honour"},{"author":"Herman Melville","category":"fiction","isbn":"0-553-21311-3","price":8.99,"title":"Moby Dick"}]}}
}
1 change: 1 addition & 0 deletions functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package jsontemplate
Loading

0 comments on commit 35cf945

Please sign in to comment.