diff --git a/cmd/protoc-gen-go-drpc/main.go b/cmd/protoc-gen-go-drpc/main.go index 39ddfbb..d66d560 100644 --- a/cmd/protoc-gen-go-drpc/main.go +++ b/cmd/protoc-gen-go-drpc/main.go @@ -11,7 +11,9 @@ import ( "strconv" "strings" + "google.golang.org/genproto/googleapis/api/annotations" "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/pluginpb" ) @@ -344,6 +346,7 @@ func (d *drpc) generateService(service *protogen.Service) { d.generateServiceRPCInterfaces(service) d.generateServiceAdapters(service) + d.generateGatewayRoutes(service) } // @@ -672,6 +675,124 @@ func (d *drpc) generateGRPCAdapter(service *protogen.Service) { d.P() } +// +// gateway route generation +// + +func getHTTPRule(method *protogen.Method) *annotations.HttpRule { + opts := method.Desc.Options() + if opts == nil { + return nil + } + ext := proto.GetExtension(opts, annotations.E_Http) + rule, _ := ext.(*annotations.HttpRule) + return rule +} + +type httpBinding struct { + method string + path string +} + +// httpBindings returns all HTTP method+path bindings declared on a +// method's google.api.http annotation, including additional_bindings. +// For example: +// +// rpc Health(HealthRequest) returns (HealthResponse) { +// option (google.api.http) = { +// get: "/_admin/v1/health" +// additional_bindings { get: "/health" } +// }; +// } +// +// produces two bindings: GET /_admin/v1/health and GET /health. +func httpBindings(rule *annotations.HttpRule) []httpBinding { + var bindings []httpBinding + if b := extractBinding(rule); b.method != "" { + bindings = append(bindings, b) + } + for _, ab := range rule.AdditionalBindings { + if b := extractBinding(ab); b.method != "" { + bindings = append(bindings, b) + } + } + return bindings +} + +func extractBinding(rule *annotations.HttpRule) httpBinding { + switch p := rule.Pattern.(type) { + case *annotations.HttpRule_Get: + return httpBinding{"GET", p.Get} + case *annotations.HttpRule_Post: + return httpBinding{"POST", p.Post} + case *annotations.HttpRule_Put: + return httpBinding{"PUT", p.Put} + case *annotations.HttpRule_Delete: + return httpBinding{"DELETE", p.Delete} + case *annotations.HttpRule_Patch: + return httpBinding{"PATCH", p.Patch} + } + return httpBinding{} +} + +func (d *drpc) GatewayRoutesFunc(service *protogen.Service) string { + return "DRPC" + service.GoName + "GatewayRoutes" +} + +// generateGatewayRoutes emits a DRPCGatewayRoutes function that +// returns one drpc.HTTPRoute per HTTP binding on the service's annotated +// methods. For example, given: +// +// service Status { +// rpc Nodes(NodesRequest) returns (NodesResponse) { +// option (google.api.http) = { get: "/_status/nodes" }; +// } +// } +// +// it emits: +// +// func DRPCStatusGatewayRoutes(client RPCStatusClient) []drpc.HTTPRoute { +// return []drpc.HTTPRoute{ +// {Method: "GET", Path: "/_status/nodes", Handler: client.Nodes}, +// } +// } +// +// Streaming RPCs and methods without HTTP annotations are skipped. +func (d *drpc) generateGatewayRoutes(service *protogen.Service) { + // Collect all methods with HTTP annotations, skipping streaming RPCs. + type routeEntry struct { + binding httpBinding + method *protogen.Method + } + var routes []routeEntry + for _, method := range service.Methods { + if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() { + continue + } + rule := getHTTPRule(method) + if rule == nil { + continue + } + for _, b := range httpBindings(rule) { + routes = append(routes, routeEntry{b, method}) + } + } + if len(routes) == 0 { + return + } + + httpRouteType := d.Ident("storj.io/drpc", "HTTPRoute") + + d.P("func ", d.GatewayRoutesFunc(service), "(client ", d.RPCClientIface(service), ") []", httpRouteType, " {") + d.P("return []", httpRouteType, "{") + for _, r := range routes { + d.P("{Method: ", strconv.Quote(r.binding.method), ", Path: ", strconv.Quote(r.binding.path), ", Handler: client.", r.method.GoName, "},") + } + d.P("}") + d.P("}") + d.P() +} + func (d *drpc) generateDRPCAdapter(service *protogen.Service) { adapter := d.DRPCClientAdapter(service) drpcClientImpl := d.ClientImpl(service) diff --git a/cmd/protoc-gen-go-drpc/protoc-gen-go-drpc b/cmd/protoc-gen-go-drpc/protoc-gen-go-drpc index 6cdc1d4..662ecd5 100755 Binary files a/cmd/protoc-gen-go-drpc/protoc-gen-go-drpc and b/cmd/protoc-gen-go-drpc/protoc-gen-go-drpc differ diff --git a/drpc.go b/drpc.go index f6f9208..5e3a743 100644 --- a/drpc.go +++ b/drpc.go @@ -10,6 +10,16 @@ import ( "github.com/zeebo/errs" ) +// HTTPRoute describes an HTTP route for a DRPC gateway endpoint. +// Generated code emits functions that return slices of these, one per +// HTTP binding on a google.api.http-annotated RPC method (including +// additional_bindings, so a single method can produce multiple entries). +type HTTPRoute struct { + Method string + Path string + Handler any +} + // These error classes represent some common errors that drpc generates. var ( Error = errs.Class("drpc") diff --git a/go.mod b/go.mod index a66c0f9..203d681 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/zeebo/assert v1.3.1 github.com/zeebo/errs v1.4.0 + google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 google.golang.org/grpc v1.57.2 google.golang.org/protobuf v1.30.0 @@ -16,5 +17,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.33.0 // indirect + google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e9d3c41..de0b284 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,10 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.57.2 h1:uw37EN34aMFFXB2QPW7Tq6tdTbind1GpRxw5aOX3a5k=