├── .travis.yml ├── .gitignore ├── chanson_benchmarks_test.go ├── chanson_examples_test.go ├── README.md ├── LICENSE ├── chanson.go └── chanson_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9.x 4 | - 1.10.x 5 | - 1.11.x 6 | - tip 7 | before_install: 8 | - go get github.com/axw/gocov/gocov 9 | - go get github.com/mattn/goveralls 10 | - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 11 | script: 12 | - $HOME/gopath/bin/goveralls -service=travis-ci 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /chanson_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package chanson 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "testing" 8 | ) 9 | 10 | func buildLargeObject(size int) map[string]string { 11 | obj := make(map[string]string, size) 12 | 13 | for i := 0; i < size; i++ { 14 | key := fmt.Sprintf("key-%d", i) 15 | val := fmt.Sprintf("val-%d", i) 16 | obj[key] = val 17 | } 18 | return obj 19 | } 20 | 21 | func BenchmarkNativeEncoder(b *testing.B) { 22 | lo := buildLargeObject(b.N) 23 | b.ResetTimer() 24 | 25 | err := json.NewEncoder(ioutil.Discard).Encode(lo) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | } 31 | 32 | func BenchmarkChanson(b *testing.B) { 33 | lo := buildLargeObject(b.N) 34 | b.ResetTimer() 35 | 36 | New(ioutil.Discard).Object(func(obj Object) { 37 | for k, v := range lo { 38 | obj.Set(k, v) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /chanson_examples_test.go: -------------------------------------------------------------------------------- 1 | package chanson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | ) 9 | 10 | // We will read from channels inside a goroutine 11 | // While we channels are open we will stream the output as json 12 | func ExampleChanson() { 13 | ch := make(chan int) 14 | go func() { 15 | ch <- 1 16 | ch <- 2 17 | ch <- 3 18 | ch <- 4 19 | close(ch) 20 | }() 21 | 22 | buf := bytes.NewBuffer(nil) 23 | cs := New(buf) 24 | cs.Array(func(a Array) { 25 | for i := range ch { 26 | a.Push(i) 27 | } 28 | }) 29 | 30 | fmt.Printf("%v", buf.String()) 31 | } 32 | 33 | func ExampleObject() { 34 | buf := bytes.NewBuffer(nil) 35 | cs := New(ioutil.Discard) 36 | cs.Object(func(obj Object) { 37 | obj.Set("foo", "bar") 38 | obj.Set("fun", func(enc *json.Encoder) { 39 | _ = enc.Encode([]int{1, 2, 3}) 40 | }) 41 | }) 42 | 43 | fmt.Printf("%v", buf.String()) 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chanson [![Build Status](https://travis-ci.org/gchaincl/chanson.svg)](https://travis-ci.org/gchaincl/chanson) [![Coverage Status](https://coveralls.io/repos/gchaincl/chanson/badge.svg?branch=coveralls&service=github)](https://coveralls.io/github/gchaincl/chanson?branch=coveralls) 2 | Package chanson provides a flexible way to construct JSON documents. 3 | As chanson populates Arrays and Objects from functions, it's perfectly suitable for streaming jsons as you build it. 4 | It is not an encoder itself, by default it relies on `json.Encoder` but its flexible enough to let you use whatever you want. 5 | 6 | # Example 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "bytes" 13 | "fmt" 14 | 15 | "github.com/gchaincl/chanson" 16 | ) 17 | 18 | func main() { 19 | ch := make(chan int) 20 | go func() { 21 | ch <- 1 22 | ch <- 2 23 | ch <- 3 24 | ch <- 4 25 | close(ch) 26 | }() 27 | 28 | buf := bytes.NewBuffer(nil) 29 | cs := chanson.New(buf) 30 | cs.Array(func(a chanson.Array) { 31 | for i := range ch { 32 | a.Push(i) 33 | } 34 | }) 35 | 36 | fmt.Printf("%v", buf.String()) 37 | } 38 | ``` 39 | 40 | For more examples and documentarion see the [Godoc](http://godoc.org/github.com/gchaincl/chanson). 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Gustavo Chaín 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /chanson.go: -------------------------------------------------------------------------------- 1 | // Package chanson provides a flexible way to construct JSON documents. 2 | // As chanson populates Arrays and Objects from functions, it's perfectly suitable for streaming jsons as you build it (see examples). 3 | // It is not an encoder it self, by default it relies on json.Encoder but its flexible enough to let you use whatever you want. 4 | package chanson 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | "strconv" 10 | ) 11 | 12 | // Chanson struct is the handler representing the current json being encoded 13 | type Chanson struct { 14 | w io.Writer 15 | enc *json.Encoder 16 | } 17 | 18 | // Value is the types that functions like Array.Push() or Object.Set() can accepts as values. 19 | // Custom Value types are: 20 | // - func(Array) 21 | // - func(Object) 22 | // - func(io.Writer) 23 | // If Value is none of the above, it will be encoded using json.Encoder 24 | type Value interface{} 25 | 26 | // New returns a new json stream. 27 | // The stream will use w for write the output 28 | func New(w io.Writer) Chanson { 29 | cs := Chanson{ 30 | w: w, 31 | enc: json.NewEncoder(w), 32 | } 33 | return cs 34 | } 35 | 36 | // Object will execute the callback inside an object context 37 | // this is: "{" f() "}" 38 | func (cs Chanson) Object(f func(Object)) { 39 | _, _ = cs.w.Write([]byte("{")) 40 | if f != nil { 41 | f(Object{cs: &cs, empty: true}) 42 | } 43 | _, _ = cs.w.Write([]byte("}")) 44 | } 45 | 46 | // Array will execute the callback inside an array context 47 | // this is: "[" f() "]" 48 | func (cs Chanson) Array(f func(Array)) { 49 | _, _ = cs.w.Write([]byte("[")) 50 | if f != nil { 51 | f(newArray(&cs)) 52 | } 53 | _, _ = cs.w.Write([]byte("]")) 54 | } 55 | 56 | // Object struct represent a Json Object ({}). 57 | type Object struct { 58 | cs *Chanson 59 | empty bool 60 | } 61 | 62 | // Set add an element into the object 63 | func (obj *Object) Set(key string, val Value) { 64 | if !obj.empty { 65 | _, _ = obj.cs.w.Write([]byte(",")) 66 | } else { 67 | obj.empty = false 68 | } 69 | 70 | _, _ = obj.cs.w.Write([]byte(strconv.Quote(key))) 71 | _, _ = obj.cs.w.Write([]byte(":")) 72 | handleValue(*obj.cs, val) 73 | } 74 | 75 | // Array struct represents a Json Array ([]). 76 | type Array struct { 77 | cs *Chanson 78 | empty bool 79 | } 80 | 81 | func newArray(cs *Chanson) Array { 82 | return Array{cs: cs, empty: true} 83 | } 84 | 85 | // Push pushes an item into the array 86 | func (a *Array) Push(val Value) { 87 | if !a.empty { 88 | _, _ = a.cs.w.Write([]byte(",")) 89 | } else { 90 | a.empty = false 91 | } 92 | 93 | handleValue(*a.cs, val) 94 | } 95 | 96 | func handleValue(cs Chanson, val Value) { 97 | switch t := val.(type) { 98 | case func(Array): 99 | cs.Array(t) 100 | case func(Object): 101 | cs.Object(t) 102 | case func(io.Writer): 103 | t(cs.w) 104 | default: 105 | err := cs.enc.Encode(val) 106 | if err != nil { 107 | //TODO: should panic?! 108 | _, _ = cs.w.Write([]byte("null")) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /chanson_test.go: -------------------------------------------------------------------------------- 1 | package chanson 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func trim(s string) string { 13 | return strings.Map(func(r rune) rune { 14 | switch r { 15 | case ' ', '\n', '\t': 16 | return -1 17 | default: 18 | return r 19 | } 20 | }, s) 21 | } 22 | 23 | func TestObjectKeyVal(t *testing.T) { 24 | buf := bytes.NewBuffer(nil) 25 | New(buf).Object(func(obj Object) { 26 | obj.Set("foo", "bar") 27 | obj.Set("fun", func(w io.Writer) { 28 | if _, err := w.Write([]byte(`"val"`)); err != nil { 29 | panic(err) 30 | } 31 | }) 32 | }) 33 | 34 | assert.Equal(t, trim(` 35 | { 36 | "foo": "bar", 37 | "fun": "val" 38 | }`), trim(buf.String())) 39 | } 40 | 41 | func TestObjectKeyEncoding(t *testing.T) { 42 | 43 | for _, test := range []struct { 44 | key string 45 | expected string 46 | }{ 47 | {"key\n", `"key\n"`}, 48 | {"key\t", `"key\t"`}, 49 | {"key\b", `"key\b"`}, 50 | {"key\f", `"key\f"`}, 51 | {"key\\", `"key\\"`}, 52 | } { 53 | buf := bytes.NewBuffer(nil) 54 | New(buf).Object(func(obj Object) { 55 | obj.Set(test.key, 0) 56 | }) 57 | 58 | assert.Equal(t, "{"+test.expected+":0}", trim(buf.String())) 59 | } 60 | } 61 | 62 | func TestArrays(t *testing.T) { 63 | buf := bytes.NewBuffer(nil) 64 | New(buf).Array(func(a Array) { 65 | a.Push(func(w io.Writer) { 66 | if _, err := w.Write([]byte("1")); err != nil { 67 | panic(err) 68 | } 69 | }) 70 | a.Push(2) 71 | a.Push(3) 72 | }) 73 | 74 | assert.Equal(t, `[1,2,3]`, trim(buf.String())) 75 | } 76 | 77 | func TestObjectSetWithDifferentValueTypes(t *testing.T) { 78 | buf := bytes.NewBuffer(nil) 79 | New(buf).Object(func(obj Object) { 80 | obj.Set("id", 10) 81 | obj.Set("array", func(arr Array) { 82 | arr.Push("foo") 83 | arr.Push("bar") 84 | }) 85 | obj.Set("obj", func(_obj Object) { 86 | _obj.Set("foo", "bar") 87 | }) 88 | }) 89 | 90 | assert.Equal(t, trim(` 91 | { 92 | "id": 10, 93 | "array": ["foo", "bar"], 94 | "obj": {"foo":"bar"} 95 | }`), trim(buf.String())) 96 | } 97 | 98 | func TestWritesNullWhenValueEncodingFails(t *testing.T) { 99 | buf := bytes.NewBuffer(nil) 100 | New(buf).Object(func(obj Object) { 101 | // func(){} will return an error when tried to be json.Encoder#Encode() 102 | val := func() {} 103 | obj.Set("key", val) 104 | }) 105 | 106 | assert.Equal(t, `{"key":null}`, trim(buf.String())) 107 | } 108 | 109 | func TestWithChannels(t *testing.T) { 110 | intCh := make(chan int, 5) 111 | boolCh := make(chan bool, 2) 112 | 113 | go func() { 114 | boolCh <- true 115 | for i := 0; i < cap(intCh); i++ { 116 | intCh <- i 117 | } 118 | boolCh <- false 119 | 120 | close(intCh) 121 | close(boolCh) 122 | 123 | }() 124 | 125 | buf := bytes.NewBuffer(nil) 126 | New(buf).Object(func(obj Object) { 127 | obj.Set("int", func(a Array) { 128 | for n := range intCh { 129 | a.Push(n) 130 | } 131 | }) 132 | 133 | obj.Set("bool", func(a Array) { 134 | for n := range boolCh { 135 | a.Push(n) 136 | } 137 | }) 138 | }) 139 | 140 | assert.Equal(t, trim(` 141 | { 142 | "int": [0,1,2,3,4], 143 | "bool": [true,false] 144 | }`), trim(buf.String())) 145 | 146 | } 147 | --------------------------------------------------------------------------------