├── .travis.yml ├── LICENSE ├── README.md ├── builder.go ├── doc.go ├── example_test.go ├── format.go ├── format_test.go ├── go.mod ├── pointer.go ├── primitive.go ├── request.go ├── request_test.go ├── slice.go └── struct.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.17 5 | - 1.16 6 | 7 | before_install: 8 | - go get github.com/axw/gocov/gocov 9 | - go get github.com/mattn/goveralls 10 | - go get golang.org/x/tools/cmd/cover 11 | script: 12 | - $HOME/gopath/bin/goveralls -service=travis-ci -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cocoon Space 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dynjson [![PkgGoDev](https://pkg.go.dev/badge/github.com/cocoonspace/dynjson)](https://pkg.go.dev/github.com/cocoonspace/dynjson) [![Build Status](https://app.travis-ci.com/cocoonspace/dynjson.svg?branch=master)](https://app.travis-ci.com/github/cocoonspace/dynjson) [![Coverage Status](https://coveralls.io/repos/github/cocoonspace/dynjson/badge.svg?branch=master)](https://coveralls.io/github/cocoonspace/dynjson?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/cocoonspace/dynjson)](https://goreportcard.com/report/github.com/cocoonspace/dynjson) 2 | 3 | Client-customizable JSON formats for dynamic APIs. 4 | 5 | ## Introduction 6 | 7 | dynjson allow APIs to return only fields selected by the API client: 8 | 9 | ``` 10 | GET https://api.example.com/v1/foos 11 | [{"id":1,foo":1,"bar":2,"baz":3}] 12 | 13 | GET https://api.example.com/v1/foos?select=foo 14 | [{"foo":1}] 15 | 16 | GET https://api.example.com/v1/foos/1?select=foo 17 | {"foo":1} 18 | ``` 19 | 20 | dynjson mimicks the original struct using the original types and json tags. 21 | The field order is the same as the select parameters. 22 | 23 | ## Installation 24 | 25 | go get github.com/cocoonspace/dynjson 26 | 27 | ## Examples 28 | 29 | ```go 30 | type APIResult struct { 31 | Foo int `json:"foo"` 32 | Bar string `json:"bar"` 33 | } 34 | 35 | f := dynjson.NewFormatter() 36 | 37 | res := &APIResult{Foo:1, Bar:"bar"} 38 | o, err := f.Format(res, dynjson.FieldsFromRequest(r)) 39 | if err != nil { 40 | // handle error 41 | } 42 | err = json.NewEncoder(w).Encode(o) // {"foo": 1} 43 | ``` 44 | 45 | With struct fields: 46 | 47 | 48 | ```go 49 | type APIResult struct { 50 | Foo int `json:"foo"` 51 | Bar APIIncluded `json:"bar"` 52 | } 53 | 54 | type APIIncluded struct { 55 | BarFoo int `json:"barfoo"` 56 | BarBar string `json:"barbar"` 57 | } 58 | 59 | f := dynjson.NewFormatter() 60 | 61 | res := &APIResult{Foo: 1, Bar: APIIncluded{BarFoo:1, BarBar: "bar"}} 62 | o, err := f.Format(res, []string{"foo", "bar.barfoo"}) 63 | if err != nil { 64 | // handle error 65 | } 66 | err = json.NewEncoder(w).Encode(o) // {"foo": 1, "bar":{"barfoo": 1}} 67 | ``` 68 | 69 | With slices: 70 | 71 | ```go 72 | type APIResult struct { 73 | Foo int `json:"foo"` 74 | Bar string `json:"bar"` 75 | } 76 | 77 | f := dynjson.NewFormatter() 78 | 79 | res := []APIResult{{Foo: 1, Bar: "bar"}} 80 | o, err := f.Format(res, []string{"foo"}) 81 | if err != nil { 82 | // handle error 83 | } 84 | err = json.NewEncoder(w).Encode(o) // [{"foo": 1}] 85 | ``` 86 | 87 | 88 | ```go 89 | type APIResult struct { 90 | Foo int `json:"foo"` 91 | Bar []APIItem `json:"bar"` 92 | } 93 | 94 | type APIItem struct { 95 | BarFoo int `json:"barfoo"` 96 | BarBar string `json:"barbar"` 97 | } 98 | 99 | f := dynjson.NewFormatter() 100 | 101 | res := &APIResult{Foo: 1, Bar: []APIItem{{BarFoo: 1, BarBar: "bar"}}} 102 | o, err := f.Format(res, []string{"foo", "bar.barfoo"}) 103 | if err != nil { 104 | // handle error 105 | } 106 | err = json.NewEncoder(w).Encode(o) // {"foo": 1, "bar":[{"barfoo": 1}]} 107 | ``` 108 | 109 | ## Limitations 110 | 111 | * Anonymous fields without a json tag (embedded by the Go JSON encoder in the enclosing struct) are not supported, 112 | * Maps are copied as is, you cannot filter map contents using `map_field_name.map_key`. 113 | 114 | ## Performance impact 115 | 116 | ``` 117 | BenchmarkFormat_Fields 118 | BenchmarkFormat_Fields-8 2466639 480 ns/op 184 B/op 7 allocs/op 119 | BenchmarkFormat_NoFields 120 | BenchmarkFormat_NoFields-8 5255031 232 ns/op 32 B/op 1 allocs/op 121 | BenchmarkRawJSON 122 | BenchmarkRawJSON-8 5351313 223 ns/op 32 B/op 1 allocs/op 123 | ``` 124 | 125 | ## Contribution guidelines 126 | 127 | Contributions are welcome, as long as: 128 | * unit tests & comments are included, 129 | * no external package is used. 130 | 131 | ## License 132 | 133 | MIT - See LICENSE -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type builder interface { 8 | build(fields []string, prefix string) (formatter, error) 9 | } 10 | 11 | func makeBuilder(t reflect.Type) (builder, error) { 12 | switch t.Kind() { 13 | case reflect.Struct: 14 | return makeStructBuilder(t) 15 | case reflect.Ptr: 16 | if t.Elem().Kind() != reflect.Struct { 17 | return makePrimitiveBuilder(t) 18 | } 19 | return makePointerBuilder(t) 20 | case reflect.Slice: 21 | if t.Elem().Kind() != reflect.Struct { 22 | return makePrimitiveBuilder(t) 23 | } 24 | return makeSliceBuilder(t) 25 | default: 26 | return makePrimitiveBuilder(t) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package dynjson allow APIs to return only fields selected by the API client: 2 | // 3 | // GET https://api.example.com/v1/foos 4 | // [{"id":1,foo":1,"bar":2,"baz":3}] 5 | // 6 | // GET https://api.example.com/v1/foos?select=foo 7 | // [{"foo":1}] 8 | // 9 | // GET https://api.example.com/v1/foos/1?select=foo 10 | // {"foo":1} 11 | // 12 | // dynjson mimicks the original struct using the original types and json tags. 13 | // The field order is the same as the select parameters. 14 | package dynjson 15 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func ExampleFormatter_Format() { 9 | var w http.ResponseWriter 10 | var r *http.Request 11 | 12 | type APIResult struct { 13 | Foo int `json:"foo"` 14 | Bar string `json:"bar"` 15 | } 16 | 17 | f := NewFormatter() 18 | 19 | res := &APIResult{Foo: 1, Bar: "bar"} 20 | o, err := f.Format(res, FieldsFromRequest(r)) 21 | if err != nil { 22 | // handle error 23 | } 24 | err = json.NewEncoder(w).Encode(o) // {"foo": 1} 25 | if err != nil { 26 | // handle error 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | type formatter interface { 10 | typ() reflect.Type 11 | format(src reflect.Value) (reflect.Value, error) 12 | } 13 | 14 | // Formatter is a dynamic API format formatter. 15 | type Formatter struct { 16 | mu sync.Mutex 17 | builders map[reflect.Type]builder 18 | formatters map[reflect.Type]map[string]formatter 19 | } 20 | 21 | // NewFormatter creates a new formatter. 22 | func NewFormatter() *Formatter { 23 | return &Formatter{ 24 | builders: map[reflect.Type]builder{}, 25 | formatters: map[reflect.Type]map[string]formatter{}, 26 | } 27 | } 28 | 29 | // Format formats either a struct or a slice, returning only the selected fields (or all if none specified). 30 | func (f *Formatter) Format(o interface{}, fields []string) (interface{}, error) { 31 | if len(fields) == 0 { 32 | return o, nil 33 | } 34 | v := reflect.ValueOf(o) 35 | t := v.Type() 36 | f.mu.Lock() 37 | defer f.mu.Unlock() 38 | b := f.builders[t] 39 | if b == nil { 40 | var err error 41 | b, err = makeBuilder(t) 42 | if err != nil { 43 | return nil, err 44 | } 45 | f.builders[t] = b 46 | f.formatters[t] = map[string]formatter{} 47 | } 48 | key := strings.Join(fields, ",") 49 | ff := f.formatters[t][key] 50 | if ff == nil { 51 | var err error 52 | ff, err = b.build(fields, "") 53 | if err != nil { 54 | return nil, err 55 | } 56 | f.formatters[t][key] = ff 57 | } 58 | v, err := ff.format(v) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return v.Interface(), nil 63 | } 64 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestFormat(t *testing.T) { 12 | var one = 1 13 | var tests = []struct { 14 | src interface{} 15 | format string 16 | output string 17 | err string 18 | }{ 19 | { 20 | src: struct{ Foo int }{}, 21 | format: "bar", 22 | err: "field 'bar' does not exist", 23 | }, 24 | { 25 | src: struct { 26 | foo int 27 | }{}, 28 | format: "foo", 29 | err: "field 'foo' does not exist", 30 | }, 31 | { 32 | src: struct { 33 | Foo int `json:"foo"` 34 | }{}, 35 | format: "foo.bar", 36 | err: "field 'foo.bar' does not exist", 37 | }, 38 | { 39 | src: struct { 40 | Foo struct { 41 | Bar int `json:"bar"` 42 | } `json:"foo"` 43 | }{}, 44 | format: "foo.baz", 45 | err: "field 'foo.baz' does not exist", 46 | }, 47 | { 48 | src: struct { 49 | Foo struct { 50 | Bar int `json:"bar"` 51 | } `json:"foo"` 52 | }{}, 53 | format: "foo..baz", 54 | err: "field 'foo.' does not exist", 55 | }, 56 | { 57 | src: struct{ Foo int }{Foo: 1}, 58 | format: "", 59 | output: `{"Foo":1}`, 60 | }, 61 | { 62 | src: struct{ Foo int }{Foo: 1}, 63 | format: "Foo", 64 | output: `{"Foo":1}`, 65 | }, 66 | { 67 | src: struct { 68 | Foo int `json:"foo"` 69 | }{Foo: 1}, 70 | format: "", 71 | output: `{"foo":1}`, 72 | }, 73 | { 74 | src: struct { 75 | Foo int `json:"foo"` 76 | }{Foo: 1}, 77 | format: "foo", 78 | output: `{"foo":1}`, 79 | }, 80 | { 81 | src: struct { 82 | Foo int `json:"foo,omitempty"` 83 | }{}, 84 | format: "foo", 85 | output: `{}`, 86 | }, 87 | { 88 | src: struct { 89 | Foo int `json:"foo"` 90 | Bar string `json:"-"` 91 | }{Foo: 1}, 92 | format: "", 93 | output: `{"foo":1}`, 94 | }, 95 | { 96 | src: struct { 97 | Foo int `json:"foo"` 98 | Bar string `json:"bar"` 99 | }{Foo: 1, Bar: "bar"}, 100 | format: "foo", 101 | output: `{"foo":1}`, 102 | }, 103 | { 104 | src: struct { 105 | Foo int `json:"foo"` 106 | Bar string `json:"bar"` 107 | Baz int `json:"-"` 108 | }{Foo: 1, Bar: "bar", Baz: 2}, 109 | format: "foo", 110 | output: `{"foo":1}`, 111 | }, 112 | { 113 | src: struct { 114 | Foo int `json:"foo"` 115 | Bar string `json:"bar"` 116 | }{Foo: 1, Bar: "bar"}, 117 | format: "", 118 | output: `{"foo":1,"bar":"bar"}`, 119 | }, 120 | { 121 | src: struct { 122 | Foo int `json:"foo"` 123 | Bar string `json:"bar"` 124 | }{Foo: 1, Bar: "bar"}, 125 | format: "foo,bar", 126 | output: `{"foo":1,"bar":"bar"}`, 127 | }, 128 | { 129 | src: struct { 130 | Foo int `json:"foo"` 131 | Bar string `json:"bar"` 132 | }{Foo: 1, Bar: "bar"}, 133 | format: "bar,foo", 134 | output: `{"bar":"bar","foo":1}`, 135 | }, 136 | { 137 | src: struct { 138 | Foo struct { 139 | Bar int `json:"bar"` 140 | Baz string `json:"baz"` 141 | } `json:"foo"` 142 | }{ 143 | Foo: struct { 144 | Bar int `json:"bar"` 145 | Baz string `json:"baz"` 146 | }{ 147 | Bar: 1, 148 | Baz: "baz", 149 | }, 150 | }, 151 | format: "", 152 | output: `{"foo":{"bar":1,"baz":"baz"}}`, 153 | }, 154 | { 155 | src: struct { 156 | Foo struct { 157 | Bar struct { 158 | Baz string `json:"baz"` 159 | } `json:"bar"` 160 | } `json:"foo"` 161 | }{ 162 | Foo: struct { 163 | Bar struct { 164 | Baz string `json:"baz"` 165 | } `json:"bar"` 166 | }{ 167 | Bar: struct { 168 | Baz string `json:"baz"` 169 | }{ 170 | Baz: "baz", 171 | }, 172 | }, 173 | }, 174 | format: "foo.bar.baz", 175 | output: `{"foo":{"bar":{"baz":"baz"}}}`, 176 | }, 177 | { 178 | src: struct { 179 | Foo struct { 180 | Bar int `json:"bar"` 181 | Baz string `json:"baz"` 182 | } `json:"foo"` 183 | }{ 184 | Foo: struct { 185 | Bar int `json:"bar"` 186 | Baz string `json:"baz"` 187 | }{ 188 | Bar: 1, 189 | Baz: "baz", 190 | }, 191 | }, 192 | format: "foo.bar", 193 | output: `{"foo":{"bar":1}}`, 194 | }, 195 | { 196 | src: struct { 197 | Foo struct { 198 | Bar int `json:"bar"` 199 | } `json:"foo"` 200 | Baz string `json:"baz"` 201 | }{ 202 | Foo: struct { 203 | Bar int `json:"bar"` 204 | }{ 205 | Bar: 1, 206 | }, 207 | Baz: "baz", 208 | }, 209 | format: "", 210 | output: `{"foo":{"bar":1},"baz":"baz"}`, 211 | }, 212 | { 213 | src: struct { 214 | Foo struct { 215 | Bar int `json:"bar"` 216 | Baz string `json:"baz"` 217 | } `json:"foo"` 218 | }{ 219 | Foo: struct { 220 | Bar int `json:"bar"` 221 | Baz string `json:"baz"` 222 | }{ 223 | Bar: 1, 224 | Baz: "baz", 225 | }, 226 | }, 227 | format: "foo", 228 | output: `{"foo":{"bar":1,"baz":"baz"}}`, 229 | }, 230 | { 231 | src: struct { 232 | Foo struct { 233 | Bar int `json:"bar"` 234 | Baz string `json:"baz"` 235 | } `json:"foo"` 236 | }{ 237 | Foo: struct { 238 | Bar int `json:"bar"` 239 | Baz string `json:"baz"` 240 | }{ 241 | Bar: 1, 242 | Baz: "baz", 243 | }, 244 | }, 245 | format: "foo", 246 | output: `{"foo":{"bar":1,"baz":"baz"}}`, 247 | }, 248 | { 249 | src: struct { 250 | Foo struct { 251 | Bar int `json:"bar"` 252 | } `json:"foo"` 253 | Baz string `json:"baz"` 254 | }{ 255 | Foo: struct { 256 | Bar int `json:"bar"` 257 | }{ 258 | Bar: 1, 259 | }, 260 | Baz: "baz", 261 | }, 262 | format: "foo.bar,baz", 263 | output: `{"foo":{"bar":1},"baz":"baz"}`, 264 | }, 265 | { 266 | src: struct { 267 | Foo *int `json:"foo,omitempty"` 268 | Bar int `json:"bar"` 269 | }{ 270 | Foo: &one, 271 | Bar: 1, 272 | }, 273 | format: "foo,bar", 274 | output: `{"foo":1,"bar":1}`, 275 | }, 276 | { 277 | src: struct { 278 | Foo *int `json:"foo,omitempty"` 279 | Bar int `json:"bar"` 280 | }{ 281 | Foo: nil, 282 | Bar: 1, 283 | }, 284 | format: "foo,bar", 285 | output: `{"bar":1}`, 286 | }, 287 | { 288 | src: struct { 289 | Foo *struct { 290 | Bar int `json:"bar"` 291 | } `json:"foo,omitempty"` 292 | Baz string `json:"baz"` 293 | }{ 294 | Foo: &struct { 295 | Bar int `json:"bar"` 296 | }{ 297 | Bar: 1, 298 | }, 299 | Baz: "baz", 300 | }, 301 | format: "foo.bar,baz", 302 | output: `{"foo":{"bar":1},"baz":"baz"}`, 303 | }, 304 | { 305 | src: struct { 306 | Foo *struct { 307 | Bar int `json:"bar"` 308 | } `json:"foo,omitempty"` 309 | Baz string `json:"baz"` 310 | }{ 311 | Foo: nil, 312 | Baz: "baz", 313 | }, 314 | format: "foo.bar,baz", 315 | output: `{"baz":"baz"}`, 316 | }, 317 | { 318 | src: struct { 319 | Foo struct { 320 | Foo int `json:"foo"` 321 | Bar int `json:"bar"` 322 | } `json:"foo"` 323 | Baz string `json:"baz"` 324 | }{ 325 | Foo: struct { 326 | Foo int `json:"foo"` 327 | Bar int `json:"bar"` 328 | }{ 329 | Foo: 1, 330 | Bar: 2, 331 | }, 332 | Baz: "baz", 333 | }, 334 | format: "foo.bar,baz,foo.foo", 335 | output: `{"foo":{"bar":2,"foo":1},"baz":"baz"}`, 336 | }, 337 | { 338 | src: struct { 339 | Foo struct { 340 | Foo int `json:"foo"` 341 | Bar int `json:"bar"` 342 | } `json:"foo"` 343 | Baz string `json:"baz"` 344 | }{ 345 | Foo: struct { 346 | Foo int `json:"foo"` 347 | Bar int `json:"bar"` 348 | }{ 349 | Foo: 1, 350 | Bar: 2, 351 | }, 352 | Baz: "baz", 353 | }, 354 | format: "foo.bar,baz,foo.foo", 355 | output: `{"foo":{"bar":2,"foo":1},"baz":"baz"}`, 356 | }, 357 | { 358 | src: []struct { 359 | Foo int `json:"foo"` 360 | Bar int `json:"bar"` 361 | }{ 362 | { 363 | Foo: 1, 364 | Bar: 2, 365 | }, 366 | }, 367 | format: "foo", 368 | output: `[{"foo":1}]`, 369 | }, 370 | { 371 | src: struct { 372 | Foo []int `json:"foo"` 373 | Bar int `json:"bar"` 374 | Baz int `json:"baz"` 375 | }{ 376 | Foo: []int{1, 2, 3}, 377 | Bar: 1, 378 | Baz: 2, 379 | }, 380 | format: "foo", 381 | output: `{"foo":[1,2,3]}`, 382 | }, 383 | { 384 | src: struct { 385 | Foo []struct { 386 | Bar int `json:"bar"` 387 | Baz int `json:"baz"` 388 | } `json:"foo"` 389 | }{ 390 | Foo: []struct { 391 | Bar int `json:"bar"` 392 | Baz int `json:"baz"` 393 | }{{ 394 | Bar: 1, 395 | Baz: 2, 396 | }}, 397 | }, 398 | format: "foo.bar", 399 | output: `{"foo":[{"bar":1}]}`, 400 | }, 401 | } 402 | for i, tt := range tests { 403 | t.Run(fmt.Sprintf("test #%d", i), func(t *testing.T) { 404 | f := NewFormatter() 405 | var fields []string 406 | if tt.format != "" { 407 | fields = strings.Split(tt.format, ",") 408 | } 409 | o, err := f.Format(tt.src, fields) 410 | if tt.err != "" { 411 | if err == nil { 412 | t.FailNow() 413 | } 414 | if tt.err != err.Error() { 415 | t.Errorf("Returned error '%v', expected '%s'", err, tt.err) 416 | } 417 | } else { 418 | if err != nil { 419 | t.Error("Should not have returned", err) 420 | } 421 | buf, err := json.Marshal(o) 422 | if err != nil { 423 | t.Error("Should not have returned", err) 424 | } 425 | if tt.output != string(buf) { 426 | t.Errorf("Returned '%s', expected '%s'", string(buf), tt.output) 427 | } 428 | } 429 | }) 430 | } 431 | } 432 | 433 | func TestFormatAnonymous(t *testing.T) { 434 | type Embedded struct { 435 | Foo int `json:"foo"` 436 | } 437 | src := struct { 438 | Embedded `json:"foo"` 439 | Bar int `json:"bar"` 440 | }{ 441 | Embedded: Embedded{Foo: 1}, 442 | Bar: 2, 443 | } 444 | f := NewFormatter() 445 | o, err := f.Format(src, []string{"foo.foo", "bar"}) 446 | if err != nil { 447 | t.Error("Should not have returned", err) 448 | } 449 | buf, err := json.Marshal(o) 450 | if err != nil { 451 | t.Error("Should not have returned", err) 452 | } 453 | if string(buf) != `{"foo":{"foo":1},"bar":2}` { 454 | t.Errorf("Returned '%s', expected '%s'", string(buf), `{"foo":1,"bar":2}`) 455 | } 456 | } 457 | 458 | func TestFormatRecursion(t *testing.T) { 459 | type Recursive struct { 460 | Foo *Recursive `json:"foo"` 461 | Bar int `json:"bar"` 462 | } 463 | src := Recursive{Bar: 1} 464 | f := NewFormatter() 465 | o, err := f.Format(src, []string{"bar"}) 466 | if err != nil { 467 | t.Error("Should not have returned", err) 468 | } 469 | buf, err := json.Marshal(o) 470 | if err != nil { 471 | t.Error("Should not have returned", err) 472 | } 473 | if string(buf) != `{"bar":1}` { 474 | t.Errorf("Returned '%s', expected '%s'", string(buf), `{"bar":1}`) 475 | } 476 | } 477 | 478 | func TestMultipleFields(t *testing.T) { 479 | src := struct { 480 | Foo int `json:"foo"` 481 | Bar string `json:"bar"` 482 | }{Foo: 1, Bar: "bar"} 483 | f := NewFormatter() 484 | _, err := f.Format(src, []string{"foo", "foo"}) 485 | if err == nil { 486 | t.Error("Expected error but returned nil") 487 | } 488 | msg := "duplicate fields detected: foo" 489 | if err.Error() != msg { 490 | t.Errorf("Returned '%s', expected '%s'", err.Error(), msg) 491 | } 492 | } 493 | 494 | func BenchmarkFormat_Fields(b *testing.B) { 495 | f := NewFormatter() 496 | w := json.NewEncoder(ioutil.Discard) 497 | for i := 0; i < b.N; i++ { 498 | o, _ := f.Format(struct { 499 | Foo int 500 | Bar string 501 | }{Foo: i, Bar: "bar"}, []string{"foo", "bar"}) 502 | _ = w.Encode(o) 503 | } 504 | } 505 | 506 | func BenchmarkFormat_NoFields(b *testing.B) { 507 | f := NewFormatter() 508 | w := json.NewEncoder(ioutil.Discard) 509 | for i := 0; i < b.N; i++ { 510 | o, _ := f.Format(struct { 511 | Foo int 512 | Bar string 513 | }{Foo: i, Bar: "bar"}, nil) 514 | _ = w.Encode(o) 515 | } 516 | } 517 | 518 | func BenchmarkRawJSON(b *testing.B) { 519 | w := json.NewEncoder(ioutil.Discard) 520 | for i := 0; i < b.N; i++ { 521 | _ = w.Encode(struct { 522 | Foo int 523 | Bar string 524 | }{Foo: i, Bar: "bar"}) 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cocoonspace/dynjson 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /pointer.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type pointerFormatter struct { 8 | t reflect.Type 9 | elem formatter 10 | } 11 | 12 | func (f *pointerFormatter) typ() reflect.Type { 13 | return f.t 14 | } 15 | func (f *pointerFormatter) format(src reflect.Value) (reflect.Value, error) { 16 | if src.IsNil() { 17 | return reflect.Zero(f.t), nil 18 | } 19 | dst, err := f.elem.format(src.Elem()) 20 | if err != nil { 21 | return dst, err 22 | } 23 | return dst.Addr(), nil 24 | } 25 | 26 | type pointerBuilder struct { 27 | t reflect.Type 28 | elem *structBuilder 29 | } 30 | 31 | func (b *pointerBuilder) build(fields []string, prefix string) (formatter, error) { 32 | ef, err := b.elem.build(fields, prefix) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &pointerFormatter{t: reflect.PtrTo(ef.typ()), elem: ef}, nil 37 | } 38 | 39 | func makePointerBuilder(t reflect.Type) (*pointerBuilder, error) { 40 | eb, err := makeStructBuilder(t.Elem()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &pointerBuilder{t: t, elem: eb}, nil 45 | } 46 | -------------------------------------------------------------------------------- /primitive.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type primitiveFormatter struct { 9 | t reflect.Type 10 | } 11 | 12 | func (f *primitiveFormatter) typ() reflect.Type { 13 | return f.t 14 | } 15 | 16 | func (f *primitiveFormatter) format(src reflect.Value) (reflect.Value, error) { 17 | return src, nil 18 | } 19 | 20 | type primitiveBuilder struct { 21 | t reflect.Type 22 | } 23 | 24 | func (b *primitiveBuilder) build(fields []string, prefix string) (formatter, error) { 25 | if len(fields) > 0 { 26 | return nil, fmt.Errorf("field '%s' does not exist", prefix+fields[0]) 27 | } 28 | return &primitiveFormatter{t: b.t}, nil 29 | } 30 | 31 | func makePrimitiveBuilder(t reflect.Type) (*primitiveBuilder, error) { 32 | return &primitiveBuilder{t: t}, nil 33 | } 34 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // Option defines a FieldsFromRequest option. 10 | type Option int 11 | 12 | const ( 13 | // OptionMultipleFields expects multiple select query parameters. 14 | OptionMultipleFields Option = iota 15 | // OptionCommaList expects a single select parameter with comma separated values. 16 | OptionCommaList 17 | ) 18 | 19 | // FieldsFromRequest returns the list of fields requested from a http.Request. 20 | // 21 | // Without opt or with OptionMultipleFields, the expected format is: 22 | // http://api.example.com/endpoint?select=foo&select=bar 23 | // 24 | // With OptionCommaList, the expected format is: 25 | // http://api.example.com/endpoint?select=foo,bar 26 | func FieldsFromRequest(r *http.Request, opt ...Option) []string { 27 | vals, err := url.ParseQuery(r.URL.RawQuery) 28 | if err != nil { 29 | return nil 30 | } 31 | if len(opt) == 1 && opt[0] == OptionCommaList && len(vals["select"]) > 0 { 32 | return strings.Split(vals["select"][0], ",") 33 | } 34 | return vals["select"] 35 | } 36 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestFieldsFromRequestNoError(t *testing.T) { 9 | { 10 | r, err := http.NewRequest(http.MethodGet, "http://api.example.com/endpoint", nil) 11 | if err != nil { 12 | t.Error("Should not have returned", err) 13 | } 14 | fields := FieldsFromRequest(r) 15 | if len(fields) != 0 { 16 | t.Error("0 fields were expected") 17 | } 18 | } 19 | { 20 | r, err := http.NewRequest(http.MethodGet, "http://api.example.com/endpoint?select=foo&select=bar", nil) 21 | if err != nil { 22 | t.Error("Should not have returned", err) 23 | } 24 | fields := FieldsFromRequest(r) 25 | if len(fields) != 2 { 26 | t.Error("2 fields were expected") 27 | } 28 | if fields[0] != "foo" || fields[1] != "bar" { 29 | t.Errorf("Expected [foo bar] but got %v", fields) 30 | } 31 | } 32 | { 33 | r, err := http.NewRequest(http.MethodGet, "http://api.example.com/endpoint?select=foo&select=bar", nil) 34 | if err != nil { 35 | t.Error("Should not have returned", err) 36 | } 37 | fields := FieldsFromRequest(r, OptionMultipleFields) 38 | if len(fields) != 2 { 39 | t.Error("2 fields were expected") 40 | } 41 | if fields[0] != "foo" || fields[1] != "bar" { 42 | t.Errorf("Expected [foo bar] but got %v", fields) 43 | } 44 | } 45 | { 46 | r, err := http.NewRequest(http.MethodGet, "http://api.example.com/endpoint?select=foo,bar", nil) 47 | if err != nil { 48 | t.Error("Should not have returned", err) 49 | } 50 | fields := FieldsFromRequest(r, OptionCommaList) 51 | if len(fields) != 2 { 52 | t.Error("2 fields were expected") 53 | } 54 | if fields[0] != "foo" || fields[1] != "bar" { 55 | t.Errorf("Expected [foo bar] but got %v", fields) 56 | } 57 | } 58 | } 59 | 60 | func TestFieldsFromRequestError(t *testing.T) { 61 | r, err := http.NewRequest(http.MethodGet, "http://api.example.com/endpoint?select=ad%f", nil) 62 | if err != nil { 63 | t.Error("Should not have returned", err) 64 | } 65 | fields := FieldsFromRequest(r) 66 | if len(fields) != 0 { 67 | t.Error("0 fields were expected") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import "reflect" 4 | 5 | type sliceFormatter struct { 6 | t reflect.Type 7 | elem formatter 8 | } 9 | 10 | func (f *sliceFormatter) typ() reflect.Type { 11 | return f.t 12 | } 13 | 14 | func (f *sliceFormatter) format(src reflect.Value) (reflect.Value, error) { 15 | dst := reflect.MakeSlice(f.t, src.Len(), src.Len()) 16 | for i := 0; i < src.Len(); i++ { 17 | dv, err := f.elem.format(src.Index(i)) 18 | if err != nil { 19 | return dv, err 20 | } 21 | dst.Index(i).Set(dv) 22 | } 23 | return dst, nil 24 | } 25 | 26 | type sliceBuilder struct { 27 | t reflect.Type 28 | elem *structBuilder 29 | } 30 | 31 | func (b *sliceBuilder) build(fields []string, prefix string) (formatter, error) { 32 | et, err := b.elem.build(fields, prefix) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &sliceFormatter{t: reflect.SliceOf(et.typ()), elem: et}, nil 37 | } 38 | 39 | func makeSliceBuilder(t reflect.Type) (*sliceBuilder, error) { 40 | elemBuilder, err := makeStructBuilder(t.Elem()) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &sliceBuilder{t: t, elem: elemBuilder}, nil 45 | } 46 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | package dynjson 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type mapping struct { 10 | src reflect.StructField 11 | dst reflect.StructField 12 | format formatter 13 | } 14 | 15 | type structFormatter struct { 16 | t reflect.Type 17 | mappings map[string]mapping 18 | } 19 | 20 | func (f *structFormatter) typ() reflect.Type { 21 | return f.t 22 | } 23 | 24 | func (f *structFormatter) format(src reflect.Value) (reflect.Value, error) { 25 | pdst := reflect.New(f.t) 26 | dst := pdst.Elem() 27 | for key := range f.mappings { 28 | sv := src.FieldByIndex(f.mappings[key].src.Index) 29 | dv, err := f.mappings[key].format.format(sv) 30 | if err != nil { 31 | return reflect.Value{}, err 32 | } 33 | dst.FieldByIndex(f.mappings[key].dst.Index).Set(dv) 34 | } 35 | return dst, nil 36 | } 37 | 38 | type structBuilder struct { 39 | t reflect.Type 40 | builders map[string]builder 41 | tags map[string]string 42 | fields map[string]reflect.StructField 43 | } 44 | 45 | func (b *structBuilder) build(fields []string, prefix string) (formatter, error) { 46 | if len(fields) == 0 { 47 | return &primitiveFormatter{t: b.t}, nil 48 | } 49 | err := detectDuplicateFields(fields) 50 | if err != nil { 51 | return nil, err 52 | } 53 | var lf []reflect.StructField 54 | mappings := map[string]mapping{} 55 | for _, field := range fields { 56 | var ( 57 | subfields []string 58 | ) 59 | if idx := strings.Index(field, "."); idx != -1 { 60 | tag := field[:idx] 61 | if _, found := mappings[tag]; found { 62 | continue 63 | } 64 | for _, subfield := range fields { 65 | if strings.HasPrefix(subfield, field[:idx+1]) { 66 | subfields = append(subfields, subfield[idx+1:]) 67 | } 68 | } 69 | field = tag 70 | } 71 | subb := b.builders[field] 72 | if subb == nil { 73 | return nil, fmt.Errorf("field '%s' does not exist", prefix+field) 74 | } 75 | fmter, err := subb.build(subfields, prefix+field+".") 76 | if err != nil { 77 | return nil, err 78 | } 79 | sf := reflect.StructField{ 80 | Name: strings.ToUpper(field), 81 | Tag: reflect.StructTag(`json:"` + b.tags[field] + `"`), 82 | Type: fmter.typ(), 83 | Anonymous: b.fields[field].Anonymous, 84 | } 85 | lf = append(lf, sf) 86 | sf.Index = []int{len(lf) - 1} 87 | mappings[field] = mapping{ 88 | src: b.fields[field], 89 | dst: sf, 90 | format: fmter, 91 | } 92 | } 93 | return &structFormatter{t: reflect.StructOf(lf), mappings: mappings}, nil 94 | } 95 | 96 | func makeStructBuilder(t reflect.Type) (*structBuilder, error) { 97 | sb := structBuilder{ 98 | t: t, 99 | builders: map[string]builder{}, 100 | tags: map[string]string{}, 101 | fields: map[string]reflect.StructField{}, 102 | } 103 | for i := 0; i < t.NumField(); i++ { 104 | fld := t.Field(i) 105 | if fld.Type.Kind() == reflect.Ptr && fld.Type.Elem() == t { 106 | continue 107 | } 108 | tag := fld.Tag.Get("json") 109 | if tag == "-" || fld.PkgPath != "" { 110 | continue 111 | } 112 | if tag == "" { 113 | tag = fld.Name 114 | } 115 | field := tag 116 | if idx := strings.Index(field, ","); idx != -1 { 117 | field = field[:idx] 118 | } 119 | ssb, err := makeBuilder(fld.Type) 120 | if err != nil { 121 | return nil, err 122 | } 123 | sb.builders[field] = ssb 124 | sb.tags[field] = tag 125 | sb.fields[field] = fld 126 | } 127 | return &sb, nil 128 | } 129 | 130 | // detectDuplicateFields returns an error if passed the same field more than once. 131 | func detectDuplicateFields(fields []string) error { 132 | h := make(map[string]int) 133 | for _, f := range fields { 134 | h[f]++ 135 | } 136 | var e []string 137 | for f, count := range h { 138 | if count > 1 { 139 | e = append(e, f) 140 | } 141 | } 142 | if len(e) > 0 { 143 | return fmt.Errorf("duplicate fields detected: %s", strings.Join(e, ", ")) 144 | } 145 | return nil 146 | } 147 | --------------------------------------------------------------------------------