Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility enable emit unpopulated and default values. #6

Merged
merged 4 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Usage of ./grpc-rest-proxy:
--transport.http.server.gracefulTimeout duration graceful timeout (default 5s)
--transport.http.server.readHeaderTimeout duration read header timeout (default 5s)
--transport.http.server.readTimeout duration read timeout (default 10s)
--service.jsonencoder.emitUnpopulated emit unpopulated fields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add more detailed description for new cli flags. Imagine user who is using this cli app for the first time.

--service.jsonencoder.emitDefaultValues emit default values
-v, --version print version
```

Expand Down
2 changes: 2 additions & 0 deletions cmd/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

grpcClient "github.com/eset/grpc-rest-proxy/pkg/gateway/grpc"
"github.com/eset/grpc-rest-proxy/pkg/repository/descriptors"
"github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder"
"github.com/eset/grpc-rest-proxy/pkg/service/protoparser"
"github.com/eset/grpc-rest-proxy/pkg/transport"
"github.com/eset/grpc-rest-proxy/pkg/transport/http"
Expand Down Expand Up @@ -74,6 +75,7 @@ func (app *App) createHTTPServer() {
routerContext := &transport.Context{
Router: app.router,
GrcpClient: app.gateways.grpcClient,
Encoder: jsonencoder.NewOptions(app.conf.Service.JSONEncoder),
}
handler := transport.NewHandler(routerContext, logging.Default())
app.serverHTTP = http.NewServer(app.conf.Transport.HTTP.Server, handler)
Expand Down
6 changes: 6 additions & 0 deletions cmd/service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/eset/grpc-rest-proxy/pkg/gateway/grpc"
"github.com/eset/grpc-rest-proxy/pkg/repository/descriptors"
"github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder"
"github.com/eset/grpc-rest-proxy/pkg/transport"

"github.com/go-playground/validator/v10"
Expand All @@ -20,12 +21,17 @@ type Config struct {
Transport *transport.Config `mapstructure:"transport" validate:"required"`
Descriptors *descriptors.Config `mapstructure:"descriptors" validate:"required"`
Gateways *Gateway `mapstructure:"gateways" validate:"required"`
Service *Service `mapstructure:"service" validate:"required"`
}

type Gateway struct {
GrpcClientConfig *grpc.ClientConfig `mapstructure:"grpc"`
}

type Service struct {
JSONEncoder *jsonencoder.Config `mapstructure:"jsonencoder"`
}

func (c *Config) validate() error {
validate := validator.New()
if err := validate.Struct(c); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (
grpcServerAddr = "0.0.0.0:50051"
tls = false
tlsSkipverify = false
defaultEmitUnpopulated = false
defaultEmitDefaultValues = false
)

var (
Expand Down Expand Up @@ -58,6 +60,10 @@ func main() {
pflag.Duration("gateways.grpc.client.requestTimeout", defaultRequestTimeout, "requests timeout")
pflag.Bool("gateways.grpc.client.tls", tls, "use TLS for gRPC connection")
pflag.Bool("gateways.grpc.client.tlsSkipverify", tlsSkipverify, "skip TLS verification")

pflag.Bool("service.jsonencoder.emitUnpopulated", defaultEmitUnpopulated, "emit unpopulated fields")
pflag.Bool("service.jsonencoder.emitDefaultValues", defaultEmitDefaultValues, "emit default values")

pflag.BoolP("version", "v", false, "print version")
configFile := pflag.StringP("config", "c", "", "path to config file")

Expand Down
35 changes: 35 additions & 0 deletions pkg/service/jsonencoder/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2024 ESET
// See LICENSE file for redistribution.

package jsonencoder

import (
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"

jErrors "github.com/juju/errors"
)

type Config struct {
EmitUnpopulated bool `mapstructure:"emitUnpopulated"`
EmitDefaultValues bool `mapstructure:"emitDefaultValues"`
}

type Encoder struct {
opts protojson.MarshalOptions
}

func NewOptions(cfg *Config) Encoder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename NewOptions() to New().

Compare

encoder := jsonencoder.NewOptions(cfg)

vs

encoder := jsonencoder.New(cfg)

return Encoder{
opts: protojson.MarshalOptions{EmitUnpopulated: cfg.EmitUnpopulated, EmitDefaultValues: cfg.EmitDefaultValues},
}
}

func (e Encoder) Format(m proto.Message) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Change return value to ([]byte, error). This way returned value of []byte can be used to write HTTP response body in w.Write() without need to convert []byte to string and then back to []byte.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Rename from Format to Encode. The name of struct is Encoder. Encoder is used for encoding. Formatter is used for formatting.

Compare

jsonResponse, err := jsonEncoder.Format(protoResponse)

with

jsonResponse, err := jsonEncoder.Encode(protoResponse)

response, err := e.opts.Marshal(m)
if err != nil {
return "", jErrors.Trace(err)
}

return string(response), nil
}
23 changes: 15 additions & 8 deletions pkg/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"

grpcClient "github.com/eset/grpc-rest-proxy/pkg/gateway/grpc"
"github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder"
"github.com/eset/grpc-rest-proxy/pkg/service/transformer"
routerPkg "github.com/eset/grpc-rest-proxy/pkg/transport/router"

Expand All @@ -22,13 +23,13 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/dynamicpb"
)

type Context struct {
Router *routerPkg.ReloadableRouter
GrcpClient grpcClient.ClientInterface
Encoder jsonencoder.Encoder
}

type Logger interface {
Expand Down Expand Up @@ -57,10 +58,10 @@ func getQueryVariables(queryValues url.Values) []transformer.Variable {
return queryVariables
}

func convertRequestToGRPC(route *routerPkg.Match, r *http.Request) (req *dynamicpb.Message, resp *dynamicpb.Message, err error) {
func convertRequestToGRPC(route *routerPkg.Match, r *http.Request) (req *dynamicpb.Message, err error) {
reqBody, err := io.ReadAll(r.Body)
if err != nil {
return nil, nil, jErrors.Trace(err)
return nil, jErrors.Trace(err)
}
r.Body.Close()

Expand All @@ -69,11 +70,10 @@ func convertRequestToGRPC(route *routerPkg.Match, r *http.Request) (req *dynamic

req, err = transformer.GetRPCRequest(reqBody, route.GrpcSpec.RequestDesc, route.Params, route.BodyRule)
if err != nil {
return nil, nil, jErrors.Trace(err)
return nil, jErrors.Trace(err)
}
resp = transformer.GetRPCResponse(route.GrpcSpec.ResponseDesc)

return req, resp, nil
return req, nil
}

func createRoutingEndpoint(rc *Context, logger Logger) func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -90,12 +90,13 @@ func createRoutingEndpoint(rc *Context, logger Logger) func(w http.ResponseWrite
return
}

rpcRequest, rpcResponse, err := convertRequestToGRPC(routeMatch, r)
rpcRequest, err := convertRequestToGRPC(routeMatch, r)
if err != nil {
logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err)))
w.WriteHeader(http.StatusBadRequest)
return
}
rpcResponse := transformer.GetRPCResponse(routeMatch.GrpcSpec.ResponseDesc)

var header, trailer metadata.MD
err = rc.GrcpClient.Invoke(
Expand All @@ -116,7 +117,13 @@ func createRoutingEndpoint(rc *Context, logger Logger) func(w http.ResponseWrite
}

transformer.SetRESTHeaders(w.Header(), header, trailer)
fmt.Fprint(w, protojson.Format(rpcResponse))
response, err := rc.Encoder.Format(rpcResponse)
if err != nil {
logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err)))
w.WriteHeader(http.StatusInternalServerError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return

}

fmt.Fprint(w, response)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using of fmt when writing HTTP response body. Use io.Copy, io.WriteString() or w.Write().

w.WriteHeader(http.StatusOK)
}
}
Expand Down
Loading