Skip to content

emilsjolander/goson

Repository files navigation

Goson Build Status

Goson is a small simple DSL for generating json from your Go datatypes. Supports both structs and maps as well as function/method values.

Installation

Make sure to first install Go and setup all the necessary environment variables. Visit http://golang.org/doc/install for more info. After that simply go get github.com/emilsjolander/goson.

Getting started

This is a short demonstration of the library in use. The next sections will go into detail about the API and the templating syntax.

All the code below can be found in the sample folder of the project.

Start with setting up a small Go program, something like this:

package main

import (
	"fmt"
	"github.com/emilsjolander/goson"
)

type Repo struct {
	Name  string
	Url   string
	Stars int
	Forks int
}

type User struct {
	Name  string
	Repos []Repo
}

func main() {

	user := &User{
		Name: "Emil Sjölander",
		Repos: []Repo{
			Repo{
				Name:  "goson",
				Url:   "https://github.com/emilsjolander/goson",
				Stars: 69,
				Forks: 1,
			},
			Repo{
				Name:  "StickyListHeaders",
				Url:   "https://github.com/emilsjolander/StickyListHeaders",
				Stars: 772,
				Forks: 227,
			},
			Repo{
				Name:  "android-FlipView",
				Url:   "https://github.com/emilsjolander/android-FlipView",
				Stars: 170,
				Forks: 52,
			},
		},
	}

	result, err := goson.Render("user", goson.Args{"User": user})

	if err != nil {
		panic(err)
	}

	fmt.Println(string(result))
}

The first thing we do in the above code is to import goson as well as the fmt packages. We also define 2 data types, User and Repo. These are the data types that we want to format into json. In the main function of our small sample we create an instance of User containing 3 instances of Repo. After this come the only public function of this library, the Render() function. Render takes two arguments, first the name of the template to render excluding the templates file type which should be .goson. The second argument to render is a map of argument that the tempate can make use of.

Let's take a look at user.goson to see how we define our json structure.

user: {
	name: User.Name
	repos: Repo in User.Repos {
		name: Repo.Name
		url: Repo.Url
		stars: Repo.Stars
		forks: Repo.Forks
	}
}

The Above template starts by wrapping the fields within a "user" json object, next it writes the name of the user and than itterates through the repos printing each repos name, url, stars and forks. The resulting json is the following:

{
    "user": {
        "name": "Emil Sjölander",
        "repos": [
            {
                "name": "goson",
                "url": "https://github.com/emilsjolander/goson",
                "stars": 0,
                "forks": 0
            },
            {
                "name": "StickyListHeaders",
                "url": "https://github.com/emilsjolander/StickyListHeaders",
                "stars": 722,
                "forks": 197
            },
            {
                "name": "android-FlipView",
                "url": "https://github.com/emilsjolander/android-FlipView",
                "stars": 157,
                "forks": 47
            }
        ]
    }
}

As you can see, the result is automatically wrapped inside a json object. This is to follow standard restful response formats.

API

As I hinted at during the getting started part of this readme, the API is very small. It consists of only two functions.

goson.Render(template string, args Args)
goson.RenderTo(w io.Writer, template string, args Args)

goson.RenderTo is just a conveniance function for writing the result to a io.Writer. The template parameter should be the relative filepath to the root of the project. So if you are executing main.go and your template is inside the templates folder you will want to pass "templates/my_template" to Render(). This will render the your data with the my_template.goson template which is located inside the template directory.

Args is just an alias for map[string]interface{} and accepts almost anything as an argument. Complex numbers and channels are the two common data types not currently supported.

Syntax

Goson is a fairly powerful templating language with support for everything you could want (Open a pull request if I've missed anything).

define a json key the following way.

key_name:

After a json key definition there are multiple options that can follow. A string, int, float or bool literal for example.

my_int: 5
my_float: 4.3
my_bool: true
my_string: "Hello world"

A json key can also be followed by a json object literal as below.

my_object: {
	nested_string: "Hi there!"
}

When defining a object literal you have the possibility to add a alias to a variable for the scope of the object.

my_object: Object.NestedObject.NestedObject as o {
	key: o.value
}

Object.NestedObject.NestedObject can be either a struct, a *struct or a map[string].

Looping over a collection works much in the same way.

my_array: o in Object.MyCollection {
	key: o.value
}

Object.MyCollection can be an instance of either an Array, a Slice or a goson.Collection.

A very important feature for larger applications is to write modular code. The include statement makes this possible.

include(templates/template_name, MyObject.NestedObject)

The above code will look for a template templates/template_name.goson relative to the root of the project. MyObject.NestedObject is sent as a parameter to partial template. If MyObject.NestedObject has a field called MyField the partial template will refer to it via NestedObject.MyField and not MyObject.NestedObject.MyField. The argument (in this case MyObject.NestedObject) that is sent to the partial can be either a struct, a *struct or a map[string].

Comments are also supported, both single and multi line comments. They follow the standard go syntax.

my_object: {
	//TODO add some property
	my_key: MyObject.Key
}

Why?

You might ask why should I use this over Go's built in encoding/json package? That's a fair question, you might not have any need for goson. If you are building an API server you most likely have use for goson though. The json marshaler in encoding/json in both quick and fairly easy to use but it is not flexible or secure. By not being secure I mean that it is easy to leak private field when encoding/json uses a opt-out strategy for json fields. This is where goson comes into play!

Goson lets you render the same data type into different json output depending on the situation. You might have both a public and private API, in this case you could have a templates/private/user.goson and a templates/public/user.goson template, the public template might skip some internal fields as an auth token or perhaps the id of the user. One other time where goson is very useful is in the above sample, to save space I might just want to render the url of a repo when the user of the API GETs /user/1 but when they GET /user/1/repo/1 I will render all the info attached to the repo.

Another reason to use goson is that it separates the view layer(json in this case) from the model layer. Defining the json keys within the model is against any good MVC design and should be avoided when possible.

Performance

Needing to parse a language and have a richer feature set than encoding/json does make goson slower, but not by a lot! The tokenization of templates is cached in an in memory cache the first time the template is used. This is done to avoid both regexp and io during any future renderings of that template. Because templates are often fairly small and reused via partials this caching will have close to no impact on the memory usage of your application. Adding caching of the tokenization phase has however increased the performance of goson with two orders of magnitude, even more on systems with relatively slow io. As of now encoding/json is about 4x faster than goson but that is minor considering that goson renders a normal sized json response in less than a tenth on a millisecond on the machines i have testing it on.

Here you can see some benchmarks. These can be found in the goson/testing package.

BenchmarkGosonSerialization		50000	 30997 	ns/op
BenchmarkStdlibSerialization	200000	 7698	ns/op

Contributing

Pull requests and issues are very welcome! Each pull request should come with a matching test for anything added to the library. If you make a pull request without running go test in the goson/tests i will give you a sad/angry smiley!

If you want a fix to happen sooner than later, I suggest that you make a pull request.

Preferably send pull requests early, even before you are done with the feature/fix/enhancement. This way we can discuss and help each other out :)

License

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.