diff --git a/go.mod b/go.mod index a04052e84..7203f2c6b 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/googleapis/gapic-showcase require ( cloud.google.com/go v0.102.0 + github.com/ghodss/yaml v1.0.0 github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.8 github.com/googleapis/gax-go/v2 v2.4.0 @@ -13,6 +14,7 @@ require ( github.com/spf13/viper v1.12.0 golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f + golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df google.golang.org/api v0.83.0 google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 google.golang.org/grpc v1.47.0 diff --git a/go.sum b/go.sum index c69de6c4e..c99ccc475 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,7 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/schema/google/showcase/v1beta1/showcase_v1beta1.yaml b/schema/google/showcase/v1beta1/showcase_v1beta1.yaml index d71c971bb..3b71a49b4 100644 --- a/schema/google/showcase/v1beta1/showcase_v1beta1.yaml +++ b/schema/google/showcase/v1beta1/showcase_v1beta1.yaml @@ -78,8 +78,8 @@ http: - selector: google.longrunning.Operations.ListOperations get: '/v1beta1/operations' - selector: google.longrunning.Operations.GetOperation - get: '/v1beta1/{name=/operations/*}' + get: '/v1beta1/{name=operations/**}' - selector: google.longrunning.Operations.DeleteOperation - delete: '/v1beta1/{name=/operations/*}' + delete: '/v1beta1/{name=operations/**}:delete' - selector: google.longrunning.Operations.CancelOperation - post: '/v1beta1/{name=/operations/*}:cancel' + post: '/v1beta1/{name=operations/**}:cancel' diff --git a/server/genrest/genrest.go b/server/genrest/genrest.go index 4acf6ac53..f0af103c5 100644 --- a/server/genrest/genrest.go +++ b/server/genrest/genrest.go @@ -85,6 +85,10 @@ func RegisterHandlers(router *gmux.Router, backend *services.Backend) { router.HandleFunc("/v1beta1/{parent:sessions/.+}/tests", rest.HandleListTests).Methods("GET") router.HandleFunc("/v1beta1/{name:sessions/.+/tests/.+}", rest.HandleDeleteTest).Methods("DELETE") router.HandleFunc("/v1beta1/{name:sessions/.+/tests/.+}:check", rest.HandleVerifyTest).Methods("POST") + router.HandleFunc("/v1beta1/operations", rest.HandleListOperations).Methods("GET") + router.HandleFunc("/v1beta1/{name:operations/.+}", rest.HandleGetOperation).Methods("GET") + router.HandleFunc("/v1beta1/{name:operations/.+}:delete", rest.HandleDeleteOperation).Methods("DELETE") + router.HandleFunc("/v1beta1/{name:operations/.+}:cancel", rest.HandleCancelOperation).Methods("POST") router.PathPrefix("/").HandlerFunc(rest.catchAllHandler) } diff --git a/server/genrest/operations.go b/server/genrest/operations.go new file mode 100644 index 000000000..9cfe36688 --- /dev/null +++ b/server/genrest/operations.go @@ -0,0 +1,264 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// DO NOT EDIT. This is an auto-generated file containing the REST handlers +// for service #6: "Operations" (.google.longrunning.Operations). + +package genrest + +import ( + "github.com/googleapis/gapic-showcase/util/genrest/resttools" + gmux "github.com/gorilla/mux" + longrunningpb "google.golang.org/genproto/googleapis/longrunning" + "net/http" +) + +// HandleListOperations translates REST requests/responses on the wire to internal proto messages for ListOperations +// Generated for HTTP binding pattern: "/v1beta1/operations" +func (backend *RESTBackend) HandleListOperations(w http.ResponseWriter, r *http.Request) { + urlPathParams := gmux.Vars(r) + numUrlPathParams := len(urlPathParams) + + backend.StdLog.Printf("Received %s request matching '/v1beta1/operations': %q", r.Method, r.URL) + backend.StdLog.Printf(" urlPathParams (expect 0, have %d): %q", numUrlPathParams, urlPathParams) + + if numUrlPathParams != 0 { + backend.Error(w, http.StatusBadRequest, "found unexpected number of URL variables: expected 0, have %d: %#v", numUrlPathParams, urlPathParams) + return + } + + systemParameters, queryParams, err := resttools.GetSystemParameters(r) + if err != nil { + backend.Error(w, http.StatusBadRequest, "error in query string: %s", err) + return + } + + request := &longrunningpb.ListOperationsRequest{} + if err := resttools.CheckRequestFormat(nil, r, request.ProtoReflect()); err != nil { + backend.Error(w, http.StatusBadRequest, "REST request failed format check: %s", err) + return + } + if err := resttools.PopulateSingularFields(request, urlPathParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading URL path params: %s", err) + return + } + + // TODO: Decide whether query-param value or URL-path value takes precedence when a field appears in both + if err := resttools.PopulateFields(request, queryParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading query params: %s", err) + return + } + + marshaler := resttools.ToJSON() + marshaler.UseEnumNumbers = systemParameters.EnumEncodingAsInt + requestJSON, _ := marshaler.Marshal(request) + backend.StdLog.Printf(" request: %s", requestJSON) + + response, err := backend.OperationsServer.ListOperations(r.Context(), request) + if err != nil { + backend.ReportGRPCError(w, err) + return + } + + json, err := marshaler.Marshal(response) + if err != nil { + backend.Error(w, http.StatusInternalServerError, "error json-encoding response: %s", err.Error()) + return + } + + w.Write(json) +} + +// HandleGetOperation translates REST requests/responses on the wire to internal proto messages for GetOperation +// Generated for HTTP binding pattern: "/v1beta1/{name=operations/**}" +func (backend *RESTBackend) HandleGetOperation(w http.ResponseWriter, r *http.Request) { + urlPathParams := gmux.Vars(r) + numUrlPathParams := len(urlPathParams) + + backend.StdLog.Printf("Received %s request matching '/v1beta1/{name=operations/**}': %q", r.Method, r.URL) + backend.StdLog.Printf(" urlPathParams (expect 1, have %d): %q", numUrlPathParams, urlPathParams) + + if numUrlPathParams != 1 { + backend.Error(w, http.StatusBadRequest, "found unexpected number of URL variables: expected 1, have %d: %#v", numUrlPathParams, urlPathParams) + return + } + + systemParameters, queryParams, err := resttools.GetSystemParameters(r) + if err != nil { + backend.Error(w, http.StatusBadRequest, "error in query string: %s", err) + return + } + + request := &longrunningpb.GetOperationRequest{} + if err := resttools.CheckRequestFormat(nil, r, request.ProtoReflect()); err != nil { + backend.Error(w, http.StatusBadRequest, "REST request failed format check: %s", err) + return + } + if err := resttools.PopulateSingularFields(request, urlPathParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading URL path params: %s", err) + return + } + + // TODO: Decide whether query-param value or URL-path value takes precedence when a field appears in both + excludedQueryParams := []string{"name"} + if duplicates := resttools.KeysMatchPath(queryParams, excludedQueryParams); len(duplicates) > 0 { + backend.Error(w, http.StatusBadRequest, "(QueryParamsInvalidFieldError) found keys that should not appear in query params: %v", duplicates) + return + } + if err := resttools.PopulateFields(request, queryParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading query params: %s", err) + return + } + + marshaler := resttools.ToJSON() + marshaler.UseEnumNumbers = systemParameters.EnumEncodingAsInt + requestJSON, _ := marshaler.Marshal(request) + backend.StdLog.Printf(" request: %s", requestJSON) + + response, err := backend.OperationsServer.GetOperation(r.Context(), request) + if err != nil { + backend.ReportGRPCError(w, err) + return + } + + json, err := marshaler.Marshal(response) + if err != nil { + backend.Error(w, http.StatusInternalServerError, "error json-encoding response: %s", err.Error()) + return + } + + w.Write(json) +} + +// HandleDeleteOperation translates REST requests/responses on the wire to internal proto messages for DeleteOperation +// Generated for HTTP binding pattern: "/v1beta1/{name=operations/**}:delete" +func (backend *RESTBackend) HandleDeleteOperation(w http.ResponseWriter, r *http.Request) { + urlPathParams := gmux.Vars(r) + numUrlPathParams := len(urlPathParams) + + backend.StdLog.Printf("Received %s request matching '/v1beta1/{name=operations/**}:delete': %q", r.Method, r.URL) + backend.StdLog.Printf(" urlPathParams (expect 1, have %d): %q", numUrlPathParams, urlPathParams) + + if numUrlPathParams != 1 { + backend.Error(w, http.StatusBadRequest, "found unexpected number of URL variables: expected 1, have %d: %#v", numUrlPathParams, urlPathParams) + return + } + + systemParameters, queryParams, err := resttools.GetSystemParameters(r) + if err != nil { + backend.Error(w, http.StatusBadRequest, "error in query string: %s", err) + return + } + + request := &longrunningpb.DeleteOperationRequest{} + if err := resttools.CheckRequestFormat(nil, r, request.ProtoReflect()); err != nil { + backend.Error(w, http.StatusBadRequest, "REST request failed format check: %s", err) + return + } + if err := resttools.PopulateSingularFields(request, urlPathParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading URL path params: %s", err) + return + } + + // TODO: Decide whether query-param value or URL-path value takes precedence when a field appears in both + excludedQueryParams := []string{"name"} + if duplicates := resttools.KeysMatchPath(queryParams, excludedQueryParams); len(duplicates) > 0 { + backend.Error(w, http.StatusBadRequest, "(QueryParamsInvalidFieldError) found keys that should not appear in query params: %v", duplicates) + return + } + if err := resttools.PopulateFields(request, queryParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading query params: %s", err) + return + } + + marshaler := resttools.ToJSON() + marshaler.UseEnumNumbers = systemParameters.EnumEncodingAsInt + requestJSON, _ := marshaler.Marshal(request) + backend.StdLog.Printf(" request: %s", requestJSON) + + response, err := backend.OperationsServer.DeleteOperation(r.Context(), request) + if err != nil { + backend.ReportGRPCError(w, err) + return + } + + json, err := marshaler.Marshal(response) + if err != nil { + backend.Error(w, http.StatusInternalServerError, "error json-encoding response: %s", err.Error()) + return + } + + w.Write(json) +} + +// HandleCancelOperation translates REST requests/responses on the wire to internal proto messages for CancelOperation +// Generated for HTTP binding pattern: "/v1beta1/{name=operations/**}:cancel" +func (backend *RESTBackend) HandleCancelOperation(w http.ResponseWriter, r *http.Request) { + urlPathParams := gmux.Vars(r) + numUrlPathParams := len(urlPathParams) + + backend.StdLog.Printf("Received %s request matching '/v1beta1/{name=operations/**}:cancel': %q", r.Method, r.URL) + backend.StdLog.Printf(" urlPathParams (expect 1, have %d): %q", numUrlPathParams, urlPathParams) + + if numUrlPathParams != 1 { + backend.Error(w, http.StatusBadRequest, "found unexpected number of URL variables: expected 1, have %d: %#v", numUrlPathParams, urlPathParams) + return + } + + systemParameters, queryParams, err := resttools.GetSystemParameters(r) + if err != nil { + backend.Error(w, http.StatusBadRequest, "error in query string: %s", err) + return + } + + request := &longrunningpb.CancelOperationRequest{} + if err := resttools.CheckRequestFormat(nil, r, request.ProtoReflect()); err != nil { + backend.Error(w, http.StatusBadRequest, "REST request failed format check: %s", err) + return + } + if err := resttools.PopulateSingularFields(request, urlPathParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading URL path params: %s", err) + return + } + + // TODO: Decide whether query-param value or URL-path value takes precedence when a field appears in both + excludedQueryParams := []string{"name"} + if duplicates := resttools.KeysMatchPath(queryParams, excludedQueryParams); len(duplicates) > 0 { + backend.Error(w, http.StatusBadRequest, "(QueryParamsInvalidFieldError) found keys that should not appear in query params: %v", duplicates) + return + } + if err := resttools.PopulateFields(request, queryParams); err != nil { + backend.Error(w, http.StatusBadRequest, "error reading query params: %s", err) + return + } + + marshaler := resttools.ToJSON() + marshaler.UseEnumNumbers = systemParameters.EnumEncodingAsInt + requestJSON, _ := marshaler.Marshal(request) + backend.StdLog.Printf(" request: %s", requestJSON) + + response, err := backend.OperationsServer.CancelOperation(r.Context(), request) + if err != nil { + backend.ReportGRPCError(w, err) + return + } + + json, err := marshaler.Marshal(response) + if err != nil { + backend.Error(w, http.StatusInternalServerError, "error json-encoding response: %s", err.Error()) + return + } + + w.Write(json) +} diff --git a/server/genrest/showcase-rest-sample-response.txt b/server/genrest/showcase-rest-sample-response.txt index 1436e616b..451f2b964 100644 --- a/server/genrest/showcase-rest-sample-response.txt +++ b/server/genrest/showcase-rest-sample-response.txt @@ -73,6 +73,12 @@ Testing (.google.showcase.v1beta1.Testing): .google.showcase.v1beta1.Testing.DeleteTest[0] : DELETE: "/v1beta1/{name=sessions/*/tests/*}" .google.showcase.v1beta1.Testing.VerifyTest[0] : POST: "/v1beta1/{name=sessions/*/tests/*}:check" +Operations (.google.longrunning.Operations): + .google.longrunning.Operations.ListOperations[0] : GET: "/v1beta1/operations" + .google.longrunning.Operations.GetOperation[0] : GET: "/v1beta1/{name=operations/**}" + .google.longrunning.Operations.DeleteOperation[0] : DELETE: "/v1beta1/{name=operations/**}:delete" + .google.longrunning.Operations.CancelOperation[0] : POST: "/v1beta1/{name=operations/**}:cancel" + GoModel @@ -138,7 +144,7 @@ Shim "Echo" (.google.showcase.v1beta1.Echo) ---------------------------------------- Shim "Identity" (.google.showcase.v1beta1.Identity) Imports: - emptypbpb: "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb" + emptypb: "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty" genprotopb: "github.com/googleapis/gapic-showcase/server/genproto" "github.com/googleapis/gapic-showcase/server/genproto" Handlers (5): GET /v1beta1/users func ListUsers(request genprotopb.ListUsersRequest) (response genprotopb.ListUsersResponse) {} @@ -153,13 +159,13 @@ Shim "Identity" (.google.showcase.v1beta1.Identity) PATCH /v1beta1/{user.name=users/*} func UpdateUser(request genprotopb.UpdateUserRequest) (response genprotopb.User) {} ["/" "v1beta1" "/" {user.name = ["users" "/" *]}] - DELETE /v1beta1/{name=users/*} func DeleteUser(request genprotopb.DeleteUserRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=users/*} func DeleteUser(request genprotopb.DeleteUserRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["users" "/" *]}] ---------------------------------------- Shim "Messaging" (.google.showcase.v1beta1.Messaging) Imports: - emptypbpb: "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb" + emptypb: "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty" genprotopb: "github.com/googleapis/gapic-showcase/server/genproto" "github.com/googleapis/gapic-showcase/server/genproto" longrunningpb: "google.golang.org/genproto/googleapis/longrunning" "google.golang.org/genproto/googleapis/longrunning" Handlers (21): @@ -217,19 +223,19 @@ Shim "Messaging" (.google.showcase.v1beta1.Messaging) PATCH /v1beta1/{blurb.name=users/*/profile/blurbs/*} func UpdateBlurb(request genprotopb.UpdateBlurbRequest) (response genprotopb.Blurb) {} ["/" "v1beta1" "/" {blurb.name = ["users" "/" * "/" "profile" "/" "blurbs" "/" *]}] - DELETE /v1beta1/{name=rooms/*} func DeleteRoom(request genprotopb.DeleteRoomRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=rooms/*} func DeleteRoom(request genprotopb.DeleteRoomRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["rooms" "/" *]}] - DELETE /v1beta1/{name=rooms/*/blurbs/*} func DeleteBlurb(request genprotopb.DeleteBlurbRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=rooms/*/blurbs/*} func DeleteBlurb(request genprotopb.DeleteBlurbRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["rooms" "/" * "/" "blurbs" "/" *]}] - DELETE /v1beta1/{name=users/*/profile/blurbs/*} func DeleteBlurb(request genprotopb.DeleteBlurbRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=users/*/profile/blurbs/*} func DeleteBlurb(request genprotopb.DeleteBlurbRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["users" "/" * "/" "profile" "/" "blurbs" "/" *]}] ---------------------------------------- Shim "SequenceService" (.google.showcase.v1beta1.SequenceService) Imports: - emptypbpb: "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb" + emptypb: "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty" genprotopb: "github.com/googleapis/gapic-showcase/server/genproto" "github.com/googleapis/gapic-showcase/server/genproto" Handlers (3): GET /v1beta1/{name=sequences/*/sequenceReport} func GetSequenceReport(request genprotopb.GetSequenceReportRequest) (response genprotopb.SequenceReport) {} @@ -238,13 +244,13 @@ Shim "SequenceService" (.google.showcase.v1beta1.SequenceService) POST /v1beta1/sequences func CreateSequence(request genprotopb.CreateSequenceRequest) (response genprotopb.Sequence) {} ["/" "v1beta1" "/" "sequences"] - POST /v1beta1/{name=sequences/*} func AttemptSequence(request genprotopb.AttemptSequenceRequest) (response emptypbpb.Empty) {} + POST /v1beta1/{name=sequences/*} func AttemptSequence(request genprotopb.AttemptSequenceRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["sequences" "/" *]}] ---------------------------------------- Shim "Testing" (.google.showcase.v1beta1.Testing) Imports: - emptypbpb: "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb" + emptypb: "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty" genprotopb: "github.com/googleapis/gapic-showcase/server/genproto" "github.com/googleapis/gapic-showcase/server/genproto" Handlers (8): GET /v1beta1/sessions func ListSessions(request genprotopb.ListSessionsRequest) (response genprotopb.ListSessionsResponse) {} @@ -265,9 +271,27 @@ Shim "Testing" (.google.showcase.v1beta1.Testing) POST /v1beta1/{name=sessions/*/tests/*}:check func VerifyTest(request genprotopb.VerifyTestRequest) (response genprotopb.VerifyTestResponse) {} ["/" "v1beta1" "/" {name = ["sessions" "/" * "/" "tests" "/" *]} ":" "check"] - DELETE /v1beta1/{name=sessions/*} func DeleteSession(request genprotopb.DeleteSessionRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=sessions/*} func DeleteSession(request genprotopb.DeleteSessionRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["sessions" "/" *]}] - DELETE /v1beta1/{name=sessions/*/tests/*} func DeleteTest(request genprotopb.DeleteTestRequest) (response emptypbpb.Empty) {} + DELETE /v1beta1/{name=sessions/*/tests/*} func DeleteTest(request genprotopb.DeleteTestRequest) (response emptypb.Empty) {} ["/" "v1beta1" "/" {name = ["sessions" "/" * "/" "tests" "/" *]}] +---------------------------------------- +Shim "Operations" (.google.longrunning.Operations) + Imports: + emptypb: "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty" + longrunningpb: "google.golang.org/genproto/googleapis/longrunning" "google.golang.org/genproto/googleapis/longrunning" + Handlers (4): + GET /v1beta1/operations func ListOperations(request longrunningpb.ListOperationsRequest) (response longrunningpb.ListOperationsResponse) {} +["/" "v1beta1" "/" "operations"] + + GET /v1beta1/{name=operations/**} func GetOperation(request longrunningpb.GetOperationRequest) (response longrunningpb.Operation) {} +["/" "v1beta1" "/" {name = ["operations" "/" **]}] + + POST /v1beta1/{name=operations/**}:cancel func CancelOperation(request longrunningpb.CancelOperationRequest) (response emptypb.Empty) {} +["/" "v1beta1" "/" {name = ["operations" "/" **]} ":" "cancel"] + + DELETE /v1beta1/{name=operations/**}:delete func DeleteOperation(request longrunningpb.DeleteOperationRequest) (response emptypb.Empty) {} +["/" "v1beta1" "/" {name = ["operations" "/" **]} ":" "delete"] + diff --git a/util/genrest/gomodel/gomodel.go b/util/genrest/gomodel/gomodel.go index f9b5c0797..c8d1ae0d6 100644 --- a/util/genrest/gomodel/gomodel.go +++ b/util/genrest/gomodel/gomodel.go @@ -175,6 +175,7 @@ type RESTHandler struct { GoMethod string RequestType string RequestTypePackage string + RequestTypeImport string RequestVariable string RequestBodyFieldSpec BodyFieldSpec RequestBodyFieldProtoName string diff --git a/util/genrest/gomodelcreator.go b/util/genrest/gomodelcreator.go index 3e1e0b867..7d79c151f 100644 --- a/util/genrest/gomodelcreator.go +++ b/util/genrest/gomodelcreator.go @@ -112,6 +112,7 @@ func NewGoModel(protoModel *protomodel.Model) (*gomodel.Model, error) { GoMethod: protoMethodDesc.GetName(), RequestType: inGoType, RequestTypePackage: inImports.Name, + RequestTypeImport: inImports.Path, RequestVariable: "request", RequestBodyFieldSpec: bodyFieldSpec, RequestBodyFieldProtoName: binding.BodyField, diff --git a/util/genrest/goviewcreator.go b/util/genrest/goviewcreator.go index d17870533..01216deaf 100644 --- a/util/genrest/goviewcreator.go +++ b/util/genrest/goviewcreator.go @@ -49,8 +49,7 @@ func NewView(model *gomodel.Model) (*goview.View, error) { fileImports := map[string]string{ "net/http": "", "github.com/googleapis/gapic-showcase/util/genrest/resttools": "", - "github.com/gorilla/mux": "gmux", - "github.com/googleapis/gapic-showcase/server/genproto": "genprotopb", + "github.com/gorilla/mux": "gmux", } // TODO: Properly deal with import strings. They may need to be taken out of the gomodel @@ -110,6 +109,7 @@ func NewView(model *gomodel.Model) (*goview.View, error) { source.P("") source.P(" %s := &%s.%s{}", handler.RequestVariable, handler.RequestTypePackage, handler.RequestType) + fileImports[handler.RequestTypeImport] = handler.RequestTypePackage switch handler.RequestBodyFieldSpec { case gomodel.BodyFieldAll: fileImports["bytes"] = "" diff --git a/util/genrest/protomodelcreator.go b/util/genrest/protomodelcreator.go index 0217e0f68..aea2a65dd 100644 --- a/util/genrest/protomodelcreator.go +++ b/util/genrest/protomodelcreator.go @@ -16,13 +16,20 @@ package genrest import ( "fmt" + "io/ioutil" + "os" + "github.com/ghodss/yaml" "github.com/golang/protobuf/protoc-gen-go/descriptor" "github.com/googleapis/gapic-showcase/util/genrest/internal/pbinfo" "github.com/googleapis/gapic-showcase/util/genrest/protomodel" "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/genproto/googleapis/api/serviceconfig" + "google.golang.org/genproto/googleapis/longrunning" "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/types/descriptorpb" ) @@ -50,19 +57,23 @@ func NewProtoModel(plugin *protogen.Plugin) (*protomodel.Model, error) { for _, svc := range protoFile.GetService() { serviceModel := protoModel.AddService(NewService(protoPackage, svc)) for _, method := range svc.GetMethod() { - options := method.GetOptions() - if options == nil { - continue - } - - eHTTP /*, err*/ := proto.GetExtension(method.GetOptions(), annotations.E_Http) - http := eHTTP.(*annotations.HttpRule) - rules := []*annotations.HttpRule{http} - rules = append(rules, http.GetAdditionalBindings()...) - for idxRule, oneRule := range rules { - protoModel.AccumulateError(NewServiceBinding(serviceModel, method, oneRule, idxRule)) - } + addBindingsForMethod(protoModel, serviceModel, method) + } + } + } + serviceConfig, err := GetServiceConfig(plugin) + if err != nil { + return nil, err + } + mixins := collectMixins(serviceConfig) + for _, mixinFile := range mixins { + protoPackage := *mixinFile.file.Package + for _, mixinService := range mixinFile.services { + svc := mixinService.service + serviceModel := protoModel.AddService(NewService(protoPackage, svc)) + for _, method := range mixinService.methods { + addBindingsForMethod(protoModel, serviceModel, method) } } } @@ -70,6 +81,21 @@ func NewProtoModel(plugin *protogen.Plugin) (*protomodel.Model, error) { return protoModel, protoModel.Error() } +func addBindingsForMethod(protoModel *protomodel.Model, serviceModel *protomodel.Service, method *descriptor.MethodDescriptorProto) { + options := method.GetOptions() + if options == nil { + return + } + + eHTTP /*, err*/ := proto.GetExtension(method.GetOptions(), annotations.E_Http) + http := eHTTP.(*annotations.HttpRule) + rules := []*annotations.HttpRule{http} + rules = append(rules, http.GetAdditionalBindings()...) + for idxRule, oneRule := range rules { + protoModel.AccumulateError(NewServiceBinding(serviceModel, method, oneRule, idxRule)) + } +} + //////////////////////////////////////// // Service @@ -141,3 +167,108 @@ func NewRESTRequestPattern(rule *annotations.HttpRule) (*protomodel.RESTRequestP } return binding, nil } + +//////////////////////////////////////// +// Mixins + +// GetServiceConfig reads and returns the specified service config file. +func GetServiceConfig(plugin *protogen.Plugin) (*serviceconfig.Service, error) { + // TODO: Consider getting this from the plugin options. On the + // other hand, there's only one copy of this file, so maybe + // hard-coding this location isn't terrible. + serviceConfigPath := "schema/google/showcase/v1beta1/showcase_v1beta1.yaml" + _ = plugin + + y, err := ioutil.ReadFile(serviceConfigPath) + if err != nil { + cwd, _ := os.Getwd() + return nil, fmt.Errorf("error reading service config %q (cwd==%q): %v", serviceConfigPath, cwd, err) + } + + j, err := yaml.YAMLToJSON(y) + if err != nil { + return nil, fmt.Errorf("error converting YAML to JSON: %v", err) + } + + serviceConfig := &serviceconfig.Service{} + if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(j, serviceConfig); err != nil { + return nil, fmt.Errorf("error unmarshaling service config: %v", err) + } + + // An API Service Config will always have a `name` so if it is not populated, + // it's an invalid config. + if serviceConfig.GetName() == "" { + return nil, fmt.Errorf("invalid API service config file %q", serviceConfigPath) + } + return serviceConfig, nil +} + +// Mixins is the collection of files containing methods to be mixed in. +type Mixins []*MixinFile + +// MixinFile describes a single file containins methods to be mixed in. +type MixinFile struct { + file *descriptor.FileDescriptorProto + services []*MixinService +} + +// MixinService describes a single service containing methods to be filled in +type MixinService struct { + service *descriptor.ServiceDescriptorProto + methods []*descriptor.MethodDescriptorProto +} + +// indexedRules keys HTTP rules by their selectors +type indexedRules map[string]*annotations.HttpRule + +// collectMixins collects the configured mixin APIs from the Service config and +// gathers the appropriately configured mixin methods to generate for each. +func collectMixins(serviceConfig *serviceconfig.Service) Mixins { + mixinRules := indexedRules{} + for _, rule := range serviceConfig.GetHttp().GetRules() { + mixinRules[rule.GetSelector()] = rule + } + mixins := Mixins{} + for _, api := range serviceConfig.GetApis() { + if _, ok := mixinDescriptors[api.GetName()]; ok { + mixins = append(mixins, collectMixinMethods(mixinRules, api.GetName())...) + } + } + return mixins +} + +func collectMixinMethods(mixinRules indexedRules, api string) Mixins { + files := Mixins{} + for _, file := range mixinDescriptors[api] { + fileToAdd := &MixinFile{ + file: file, + } + files = append(files, fileToAdd) + for _, service := range file.GetService() { + serviceToAdd := &MixinService{ + service: service, + } + fileToAdd.services = append(fileToAdd.services, serviceToAdd) + for _, method := range service.GetMethod() { + fqn := fmt.Sprintf("%s.%s.%s", file.GetPackage(), service.GetName(), method.GetName()) + + if rule := mixinRules[fqn]; rule != nil { + proto.SetExtension(method.Options, annotations.E_Http, rule) + serviceToAdd.methods = append(serviceToAdd.methods, method) + + } + } + } + } + return files +} + +var mixinDescriptors map[string][]*descriptor.FileDescriptorProto + +func init() { + mixinDescriptors = map[string][]*descriptor.FileDescriptorProto{ + "google.longrunning.Operations": { + protodesc.ToFileDescriptorProto(longrunning.File_google_longrunning_operations_proto), + }, + } +}