Skip to content

Commit

Permalink
Implement -fixEmptyArrays to force empty array [] instead of null
Browse files Browse the repository at this point in the history
… in JSON (#32)

Golang issue: golang/go#27589

This option will enforce server to encode nil slices as empty arrays in JSON.

This is helpful mainly in JavaScript and TypeScript clients, where a response
field of type array should never be assigned to `null` value..

Example:

func (s *Server) GetItems(ctx context.Context) ([]*Items, error) {
	var items []*Item

	// Will send {"items": []} in JSON response instead of {"items": null}.
	return items, nil
}
  • Loading branch information
VojtechVitek authored Aug 23, 2023
1 parent a5ec75a commit 49bac72
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 20 deletions.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ As you can see, the `-target` supports default `golang`, any git URI, or a local
### Set custom template variables
Change any of the following values by passing `-option="Value"` CLI flag to `webrpc-gen`.

| webrpc-gen -option | Description | Default value | Added in |
|----------------------|-----------------------------------------|--------------|----------|
| `-pkg=<name>` | package name | `"proto"` | v0.5.0 |
| `-client` | generate client code | `false` | v0.5.0 |
| `-server` | generate server code | `false` | v0.5.0 |
| `-types=false` | don't generate types | `true` | v0.13.0 |
| `-json=jsoniter` | use alternative json encoding package | `"stdlib"` | v0.12.0 |
| `-legacyErrors=true` | enable legacy errors (v0.10.0 or older) | `false` | v0.11.0 |
| webrpc-gen -option | Default | Description | Added in |
|--------------------|------------|-----------------------------------------------------------------------------|----------|
| `-pkg=<name>` | `"proto"` | package name | v0.5.0 |
| `-client` | `false` | generate client code | v0.5.0 |
| `-server` | `false` | generate server code | v0.5.0 |
| `-types=false` | `true` | don't generate types | v0.13.0 |
| `-json=jsoniter` | `"stdlib"` | use alternative json encoding package | v0.12.0 |
| `-fixEmptyArrays` | `false` | force empty array `[]` instead of `null` in JSON (see Go [#27589][go27589]) | v0.13.0 |
| `-legacyErrors` | `false` | enable legacy errors (v0.10.0 or older) | v0.11.0 |

Example:
```
Expand Down Expand Up @@ -86,3 +87,5 @@ See [_examples](./_examples)
## LICENSE

[MIT LICENSE](./LICENSE)

[go27589]: https://github.com/golang/go/issues/27589
50 changes: 44 additions & 6 deletions _examples/golang-basics/example.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions _examples/golang-basics/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestGetUser(t *testing.T) {
{
arg1 := map[string]string{"a": "1"}
user, err := client.GetUser(context.Background(), arg1, 12)
assert.Equal(t, &User{ID: 12, Username: "hihi"}, user)
assert.Equal(t, &User{ID: 12, Username: "hihi", Nicknames: []Nickname{}}, user)
assert.NoError(t, err)
}

Expand Down Expand Up @@ -81,7 +81,7 @@ func TestGetUser(t *testing.T) {
{
name, user, err := client.FindUser(context.Background(), &SearchFilter{Q: "joe"})
assert.Equal(t, "joe", name)
assert.Equal(t, &User{ID: 123, Username: "joe"}, user)
assert.Equal(t, &User{ID: 123, Username: "joe", Nicknames: []Nickname{}}, user)
assert.NoError(t, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion _examples/golang-basics/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:generate webrpc-gen -schema=example.ridl -target=../../../gen-golang -pkg=main -server -client -out=./example.gen.go -legacyErrors=true
//go:generate webrpc-gen -schema=example.ridl -target=../../../gen-golang -pkg=main -server -client -legacyErrors -fixEmptyArrays -out=./example.gen.go
package main

import (
Expand Down
3 changes: 2 additions & 1 deletion _examples/golang-imports/api.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions helpers.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{{define "helpers"}}
{{ $opts := .Opts -}}

//
// Helpers
//
Expand All @@ -24,4 +26,45 @@ var (

MethodNameCtxKey = &contextKey{"MethodName"}
)

{{- if $opts.fixEmptyArrays }}

// Copied from https://github.com/golang-cz/nilslice
func initializeNilSlices(obj interface{}) interface{} {
v := reflect.ValueOf(obj)
initializeNils(v)

return obj
}

func initializeNils(v reflect.Value) {
// Dereference pointer(s).
for v.Kind() == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}

if v.Kind() == reflect.Slice {
// Initialize a nil slice.
if v.IsNil() {
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
return
}

// Recursively iterate over slice items.
for i := 0; i < v.Len(); i++ {
item := v.Index(i)
initializeNils(item)
}
}

// Recursively iterate over struct fields.
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
initializeNils(field)
}
}
}
{{ end }}

{{ end -}}
5 changes: 3 additions & 2 deletions main.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{{- set $opts "types" (ternary (eq (default .Opts.types "true") "false") false true) -}}
{{- set $opts "json" (default .Opts.json "stdlib") -}}
{{- set $opts "importTypesFrom" (default .Opts.importTypesFrom "" ) -}}
{{- set $opts "fixEmptyArrays" (ternary (in .Opts.fixEmptyArrays "" "true") true false) -}}
{{- set $opts "legacyErrors" (ternary (in .Opts.legacyErrors "" "true") true false) -}}

{{- $typePrefix := (last (split "/" $opts.importTypesFrom)) -}}
Expand Down Expand Up @@ -92,14 +93,14 @@ func WebRPCSchemaHash() string {
{{ end -}}

{{- if $opts.server}}
{{ template "server" dict "Services" .Services "TypeMap" $typeMap "TypePrefix" $typePrefix }}
{{ template "server" dict "Services" .Services "TypeMap" $typeMap "TypePrefix" $typePrefix "Opts" $opts }}
{{ end -}}

{{ if $opts.client }}
{{ template "client" dict "Services" .Services "TypeMap" $typeMap "TypePrefix" $typePrefix }}
{{ end -}}

{{ template "helpers" . }}
{{ template "helpers" dict "Opts" $opts }}

{{- template "errors" dict "WebrpcErrors" .WebrpcErrors "SchemaErrors" .Errors "Opts" $opts "TypePrefix" $typePrefix }}

Expand Down
6 changes: 6 additions & 0 deletions server.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{{- define "server"}}
{{- $typeMap := .TypeMap -}}
{{- $typePrefix := .TypePrefix -}}
{{- $opts := .Opts -}}

{{- if .Services -}}
//
// Server
Expand Down Expand Up @@ -120,7 +122,11 @@ func (s *{{$serviceName}}) serve{{ .Name | firstLetterToUpper }}JSON(ctx context
}

{{- if .Outputs | len}}
{{ if $opts.fixEmptyArrays -}}
respBody, err := json.Marshal(initializeNilSlices(respContent))
{{ else -}}
respBody, err := json.Marshal(respContent)
{{ end -}}
if err != nil {
err = ErrorWithCause(ErrWebrpcBadResponse, fmt.Errorf("failed to marshal json response: %w", err))
RespondWithError(w, err)
Expand Down

0 comments on commit 49bac72

Please sign in to comment.