From 49bac720de04009ae8385e107b3ae2ef43ffdffd Mon Sep 17 00:00:00 2001 From: "Vojtech Vitek (golang.cz)" Date: Wed, 23 Aug 2023 19:18:51 +0200 Subject: [PATCH] Implement -fixEmptyArrays to force empty array `[]` instead of `null` in JSON (#32) Golang issue: https://github.com/golang/go/issues/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 } --- README.md | 19 ++++++---- _examples/golang-basics/example.gen.go | 50 ++++++++++++++++++++++--- _examples/golang-basics/example_test.go | 4 +- _examples/golang-basics/main.go | 2 +- _examples/golang-imports/api.gen.go | 3 +- helpers.go.tmpl | 43 +++++++++++++++++++++ main.go.tmpl | 5 ++- server.go.tmpl | 6 +++ 8 files changed, 112 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 56a85cf..4dfa35a 100644 --- a/README.md +++ b/README.md @@ -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=` | 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=` | `"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: ``` @@ -86,3 +87,5 @@ See [_examples](./_examples) ## LICENSE [MIT LICENSE](./LICENSE) + +[go27589]: https://github.com/golang/go/issues/27589 \ No newline at end of file diff --git a/_examples/golang-basics/example.gen.go b/_examples/golang-basics/example.gen.go index a81b62e..4c3a986 100644 --- a/_examples/golang-basics/example.gen.go +++ b/_examples/golang-basics/example.gen.go @@ -1,8 +1,8 @@ // example v0.0.1 87cdac57aac886a37b5caffc5962c93859970b68 // -- -// Code generated by webrpc-gen@v0.13.0-dev with ../../../gen-golang generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.14.0-dev with ../../../gen-golang generator. DO NOT EDIT. // -// webrpc-gen -schema=example.ridl -target=../../../gen-golang -pkg=main -server -client -out=./example.gen.go -legacyErrors=true +// webrpc-gen -schema=example.ridl -target=../../../gen-golang -pkg=main -server -client -legacyErrors -fixEmptyArrays -out=./example.gen.go package main import ( @@ -15,6 +15,7 @@ import ( "io/ioutil" "net/http" "net/url" + "reflect" "strings" "time" @@ -262,7 +263,7 @@ func (s *exampleServiceServer) serveStatusJSON(ctx context.Context, w http.Respo RespondWithError(w, err) return } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(initializeNilSlices(respContent)) if err != nil { err = ErrorWithCause(ErrWebrpcBadResponse, fmt.Errorf("failed to marshal json response: %w", err)) RespondWithError(w, err) @@ -314,7 +315,7 @@ func (s *exampleServiceServer) serveVersionJSON(ctx context.Context, w http.Resp RespondWithError(w, err) return } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(initializeNilSlices(respContent)) if err != nil { err = ErrorWithCause(ErrWebrpcBadResponse, fmt.Errorf("failed to marshal json response: %w", err)) RespondWithError(w, err) @@ -385,7 +386,7 @@ func (s *exampleServiceServer) serveGetUserJSON(ctx context.Context, w http.Resp RespondWithError(w, err) return } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(initializeNilSlices(respContent)) if err != nil { err = ErrorWithCause(ErrWebrpcBadResponse, fmt.Errorf("failed to marshal json response: %w", err)) RespondWithError(w, err) @@ -457,7 +458,7 @@ func (s *exampleServiceServer) serveFindUserJSON(ctx context.Context, w http.Res RespondWithError(w, err) return } - respBody, err := json.Marshal(respContent) + respBody, err := json.Marshal(initializeNilSlices(respContent)) if err != nil { err = ErrorWithCause(ErrWebrpcBadResponse, fmt.Errorf("failed to marshal json response: %w", err)) RespondWithError(w, err) @@ -711,6 +712,43 @@ var ( MethodNameCtxKey = &contextKey{"MethodName"} ) +// 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) + } + } +} + // // Errors // diff --git a/_examples/golang-basics/example_test.go b/_examples/golang-basics/example_test.go index 7eab180..1da0f52 100644 --- a/_examples/golang-basics/example_test.go +++ b/_examples/golang-basics/example_test.go @@ -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) } @@ -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) } } diff --git a/_examples/golang-basics/main.go b/_examples/golang-basics/main.go index 923826a..06666f5 100644 --- a/_examples/golang-basics/main.go +++ b/_examples/golang-basics/main.go @@ -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 ( diff --git a/_examples/golang-imports/api.gen.go b/_examples/golang-imports/api.gen.go index bc08ca8..516ca71 100644 --- a/_examples/golang-imports/api.gen.go +++ b/_examples/golang-imports/api.gen.go @@ -1,6 +1,6 @@ // example-api-service v1.0.0 f1b366018b1650b6a0a4f09ff793089ec62830a6 // -- -// Code generated by webrpc-gen@v0.13.0-dev with ../../../gen-golang generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.14.0-dev with ../../../gen-golang generator. DO NOT EDIT. // // webrpc-gen -schema=./proto/api.ridl -target=../../../gen-golang -out=./api.gen.go -pkg=main -server -client -legacyErrors=true -fmt=false package main @@ -498,6 +498,7 @@ var ( MethodNameCtxKey = &contextKey{"MethodName"} ) + // // Errors // diff --git a/helpers.go.tmpl b/helpers.go.tmpl index b74f091..54b15e8 100644 --- a/helpers.go.tmpl +++ b/helpers.go.tmpl @@ -1,4 +1,6 @@ {{define "helpers"}} +{{ $opts := .Opts -}} + // // Helpers // @@ -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 -}} diff --git a/main.go.tmpl b/main.go.tmpl index 5c80d18..4ddab9d 100644 --- a/main.go.tmpl +++ b/main.go.tmpl @@ -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)) -}} @@ -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 }} diff --git a/server.go.tmpl b/server.go.tmpl index c3bcefc..b6273fa 100644 --- a/server.go.tmpl +++ b/server.go.tmpl @@ -1,6 +1,8 @@ {{- define "server"}} {{- $typeMap := .TypeMap -}} {{- $typePrefix := .TypePrefix -}} +{{- $opts := .Opts -}} + {{- if .Services -}} // // Server @@ -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)