diff --git a/generator/cmd/protoc-gen-gclient/main.go b/generator/cmd/protoc-gen-gclient/main.go index 0ec197a06..ee253ea9f 100644 --- a/generator/cmd/protoc-gen-gclient/main.go +++ b/generator/cmd/protoc-gen-gclient/main.go @@ -73,17 +73,26 @@ func run(r io.Reader, w io.Writer, inputPath, outDir, templateDir string) error // If capture-input is set, content pass to protoc will be written to a // sample-input-{timestamp}.bin file, so that protoc does not need to be // used on future iterations. - if opts.CaptureInput { + if opts.captureInput { if err := captureInput(genReq); err != nil { return err } } - req, err := protobuf.Translate(genReq, &protobuf.Options{ + popts := &protobuf.Options{ OutDir: outDir, - Language: opts.Language, + Language: opts.language, TemplateDir: templateDir, - }) + } + if opts.serviceConfig != "" { + cfg, err := genclient.ReadServiceConfig(opts.serviceConfig) + if err != nil { + return err + } + popts.ServiceConfig = cfg + } + + req, err := protobuf.Translate(genReq, popts) if err != nil { return err } @@ -102,8 +111,9 @@ func run(r io.Reader, w io.Writer, inputPath, outDir, templateDir string) error } type protobufOptions struct { - CaptureInput bool - Language string + captureInput bool + language string + serviceConfig string } func parseOpts(optStr string) (*protobufOptions, error) { @@ -119,15 +129,17 @@ func parseOpts(optStr string) (*protobufOptions, error) { continue } switch sp[0] { + case "service-config": + opts.serviceConfig = strings.TrimSpace(sp[1]) case "capture-input": b, err := strconv.ParseBool(sp[1]) if err != nil { slog.Error("invalid bool in option string, skipping", "option", s) return nil, err } - opts.CaptureInput = b + opts.captureInput = b case "language": - opts.Language = strings.ToLower(strings.TrimSpace(sp[1])) + opts.language = strings.ToLower(strings.TrimSpace(sp[1])) default: slog.Warn("unknown option", "option", s) } diff --git a/generator/devtools/cmd/generate/main.go b/generator/devtools/cmd/generate/main.go index e2f91527c..7899270ea 100644 --- a/generator/devtools/cmd/generate/main.go +++ b/generator/devtools/cmd/generate/main.go @@ -25,10 +25,11 @@ import ( ) var ( - output = flag.String("out", "output", "the path to the output directory") - language = flag.String("language", "", "the generated language") - protoFiles = flag.String("files", "testdata/googleapis/google/cloud/secretmanager/v1/", "path to protos to generate from") - protoPath = flag.String("proto_path", "testdata/googleapis", "directory in which to search for imports") + language = flag.String("language", "", "the generated language") + output = flag.String("out", "output", "the path to the output directory") + protoFiles = flag.String("files", "testdata/googleapis/google/cloud/secretmanager/v1/", "path to protos to generate from") + protoPath = flag.String("proto_path", "testdata/googleapis", "directory in which to search for imports") + serviceConfig = flag.String("service-config", "testdata/google/cloud/secretmanager/v1/secretmanager_v1.yaml", "path to service config") ) func main() { @@ -59,7 +60,7 @@ func run(language, testdata, input, output string) error { args := []string{ "-I", testdata, fmt.Sprintf("--gclient_out=%s", output), - fmt.Sprintf("--gclient_opt=language=%s", language), + fmt.Sprintf("--gclient_opt=language=%s,service-config=%s", language, *serviceConfig), } args = append(args, files...) diff --git a/generator/go.mod b/generator/go.mod index 01038905f..97e5f500d 100644 --- a/generator/go.mod +++ b/generator/go.mod @@ -10,6 +10,7 @@ require ( github.com/pb33f/libopenapi v0.18.6 google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 google.golang.org/protobuf v1.35.1 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -20,5 +21,4 @@ require ( github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd // indirect golang.org/x/sync v0.8.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/generator/internal/genclient/service_config.go b/generator/internal/genclient/service_config.go new file mode 100644 index 000000000..fa0d922c8 --- /dev/null +++ b/generator/internal/genclient/service_config.go @@ -0,0 +1,42 @@ +// Copyright 2024 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. + +package genclient + +import ( + "fmt" + "os" + + "google.golang.org/genproto/googleapis/api/serviceconfig" + "gopkg.in/yaml.v3" +) + +func ReadServiceConfig(serviceConfigPath string) (*serviceconfig.Service, error) { + y, err := os.ReadFile(serviceConfigPath) + if err != nil { + return nil, fmt.Errorf("error reading service config: %v", err) + } + + var cfg serviceconfig.Service + if err := yaml.Unmarshal(y, &cfg); err != nil { + return nil, fmt.Errorf("error unmarshalling 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 cfg.GetName() == "" { + return nil, fmt.Errorf("invalid API service config file %q", serviceConfigPath) + } + return &cfg, nil +} diff --git a/generator/internal/genclient/service_config_test.go b/generator/internal/genclient/service_config_test.go new file mode 100644 index 000000000..9b3996d10 --- /dev/null +++ b/generator/internal/genclient/service_config_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 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. + +package genclient + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/googleapis/google-cloud-rust/generator/internal/sample" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/genproto/googleapis/api/serviceconfig" + "google.golang.org/protobuf/types/known/apipb" +) + +func TestReadServiceConfig(t *testing.T) { + const serviceConfigPath = "../../testdata/googleapis/google/cloud/secretmanager/v1/secretmanager_v1.yaml" + got, err := ReadServiceConfig(serviceConfigPath) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(sample.ServiceConfig, got, + cmpopts.IgnoreUnexported(annotations.HttpRule{}), + cmpopts.IgnoreUnexported(annotations.Http{}), + cmpopts.IgnoreUnexported(apipb.Api{}), + cmpopts.IgnoreUnexported(serviceconfig.AuthenticationRule{}), + cmpopts.IgnoreUnexported(serviceconfig.Authentication{}), + cmpopts.IgnoreUnexported(serviceconfig.BackendRule{}), + cmpopts.IgnoreUnexported(serviceconfig.Backend{}), + cmpopts.IgnoreUnexported(serviceconfig.DocumentationRule{}), + cmpopts.IgnoreUnexported(serviceconfig.Documentation{}), + cmpopts.IgnoreUnexported(serviceconfig.OAuthRequirements{}), + cmpopts.IgnoreUnexported(serviceconfig.Service{}), + ); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } +} diff --git a/generator/internal/genclient/translator/openapi/openapi.go b/generator/internal/genclient/translator/openapi/openapi.go index 9c26e6202..473bc0b52 100644 --- a/generator/internal/genclient/translator/openapi/openapi.go +++ b/generator/internal/genclient/translator/openapi/openapi.go @@ -48,6 +48,11 @@ func Translate(contents []byte, opts *Options) (*genclient.GenerateRequest, erro if err != nil { return nil, err } + api.State = &genclient.APIState{ + ServiceByID: make(map[string]*genclient.Service), + MessageByID: make(map[string]*genclient.Message), + EnumByID: make(map[string]*genclient.Enum), + } return &genclient.GenerateRequest{ API: api, Codec: codec, diff --git a/generator/internal/genclient/translator/openapi/openapi_test.go b/generator/internal/genclient/translator/openapi/openapi_test.go index 4f6b0888e..e86c7fc9a 100644 --- a/generator/internal/genclient/translator/openapi/openapi_test.go +++ b/generator/internal/genclient/translator/openapi/openapi_test.go @@ -236,7 +236,7 @@ func TestSimpleObject(t *testing.T) { } api, err := makeAPI(model) if err != nil { - t.Errorf("Error in makeAPI() %q", err) + t.Fatalf("Error in makeAPI() %q", err) } checkMessage(t, *api.Messages[0], genclient.Message{ diff --git a/generator/internal/genclient/translator/protobuf/protobuf.go b/generator/internal/genclient/translator/protobuf/protobuf.go index 9e650017f..370b5c1e3 100644 --- a/generator/internal/genclient/translator/protobuf/protobuf.go +++ b/generator/internal/genclient/translator/protobuf/protobuf.go @@ -21,6 +21,7 @@ import ( "github.com/googleapis/google-cloud-rust/generator/internal/genclient" "github.com/googleapis/google-cloud-rust/generator/internal/genclient/language" + "google.golang.org/genproto/googleapis/api/serviceconfig" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/pluginpb" @@ -47,8 +48,9 @@ const ( type Options struct { Language string // Only used for local testing - OutDir string - TemplateDir string + OutDir string + TemplateDir string + ServiceConfig *serviceconfig.Service } // Translate translates proto representation into a [genclienGenerateRequest]. @@ -100,11 +102,12 @@ func makeAPI(req *pluginpb.CodeGeneratorRequest) *genclient.API { } state.ServiceByID[service.ID] = service for _, m := range s.Method { - method := &genclient.Method{} - method.HTTPInfo = parseHTTPInfo(m.GetOptions()) - method.Name = m.GetName() - method.InputTypeID = m.GetInputType() - method.OutputTypeID = m.GetOutputType() + method := &genclient.Method{ + HTTPInfo: parseHTTPInfo(m.GetOptions()), + Name: m.GetName(), + InputTypeID: m.GetInputType(), + OutputTypeID: m.GetOutputType(), + } service.Methods = append(service.Methods, method) } fileServices = append(fileServices, service) @@ -133,7 +136,6 @@ func makeAPI(req *pluginpb.CodeGeneratorRequest) *genclient.API { } api.Services = append(api.Services, fileServices...) } - return api } @@ -258,12 +260,15 @@ func addMessageDocumentation(state *genclient.APIState, m *descriptorpb.Descript // This is a comment for a top level message state.MessageByID[mFQN].Documentation = doc } else if p[0] == messageDescriptorNestedType { + // This indicates a nested message, recurse. nmsg := m.GetNestedType()[p[1]] nmFQN := mFQN + "." + nmsg.GetName() addMessageDocumentation(state, nmsg, p[2:], doc, nmFQN) } else if len(p) == 2 && p[0] == messageDescriptorField { + // This is a comment for a field of a message state.MessageByID[mFQN].Fields[p[1]].Documentation = doc } else if p[0] == messageDescriptorEnum { + // This is a comment for a enum of a message eFQN := mFQN + "." + m.GetEnumType()[p[1]].GetName() addEnumDocumentation(state, p[2:], doc, eFQN) } else if len(p) == 2 && p[0] == messageDescriptorOneOf { diff --git a/generator/internal/sample/sample.go b/generator/internal/sample/sample.go new file mode 100644 index 000000000..d958c51a6 --- /dev/null +++ b/generator/internal/sample/sample.go @@ -0,0 +1,88 @@ +// Copyright 2024 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. + +// Package sample provides sample data for testing. +package sample + +import ( + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/genproto/googleapis/api/serviceconfig" + "google.golang.org/protobuf/types/known/apipb" +) + +var ServiceConfig = &serviceconfig.Service{ + Name: "secretmanager.googleapis.com", + Title: "Secret Manager API", + Apis: []*apipb.Api{ + { + Name: "google.cloud.secretmanager.v1.SecretManagerService", + }, + }, + Documentation: &serviceconfig.Documentation{ + Summary: "Stores sensitive data such as API keys, passwords, and certificates.\nProvides convenience while improving security.", + Rules: []*serviceconfig.DocumentationRule{ + { + Selector: "google.cloud.location.Locations.GetLocation", + Description: "Gets information about a location.", + }, + { + Selector: "google.cloud.location.Locations.ListLocations", + Description: "Lists information about the supported locations for this service.", + }, + }, + Overview: "Secret Manager Overview", + }, + Backend: &serviceconfig.Backend{ + Rules: []*serviceconfig.BackendRule{ + { + Selector: "google.cloud.location.Locations.GetLocation", + Deadline: 60, + }, + { + Selector: "google.cloud.location.Locations.ListLocations", + Deadline: 60, + }, + { + Selector: "google.cloud.secretmanager.v1.SecretManagerService.*", + Deadline: 60, + }, + }, + }, + Http: &annotations.Http{ + Rules: []*annotations.HttpRule{ + { + Selector: "google.cloud.location.Locations.GetLocation", + }, + { + Selector: "google.cloud.location.Locations.ListLocations", + }, + }, + }, + Authentication: &serviceconfig.Authentication{ + Rules: []*serviceconfig.AuthenticationRule{ + { + Selector: "google.cloud.location.Locations.GetLocation", + Oauth: &serviceconfig.OAuthRequirements{}, + }, + { + Selector: "google.cloud.location.Locations.ListLocations", + Oauth: &serviceconfig.OAuthRequirements{}, + }, + { + Selector: "google.cloud.secretmanager.v1.SecretManagerService.*", + Oauth: &serviceconfig.OAuthRequirements{}, + }, + }, + }, +} diff --git a/generator/testdata/rust/openapi/golden/model.rs b/generator/testdata/rust/openapi/golden/model.rs index 1fbebcade..51b252a4c 100755 --- a/generator/testdata/rust/openapi/golden/model.rs +++ b/generator/testdata/rust/openapi/golden/model.rs @@ -36,7 +36,7 @@ pub struct Location { /// Cross-service attributes for the location. For example /// /// {"cloud.googleapis.com/region": "us-east1"} - pub labels: Option>, + pub labels: , /// Service-specific metadata. For example the available capacity at the given /// location. @@ -78,7 +78,7 @@ pub struct Secret { /// Optional. Immutable. The replication policy of the secret data attached to the Secret. /// /// The replication policy cannot be changed after the Secret has been created. - pub replication: Option, + pub replication: , /// Output only. The time at which the Secret was created. pub create_time: Option /* TODO(#77) - handle .google.protobuf.Timestamp */, @@ -94,7 +94,7 @@ pub struct Secret { /// regular expression: `[\p{Ll}\p{Lo}\p{N}_-]{0,63}` /// /// No more than 64 labels can be assigned to a given resource. - pub labels: Option>, + pub labels: , /// Optional. A list of up to 10 Pub/Sub topics to which messages are published when /// control plane operations are called on the secret or its versions. @@ -112,7 +112,7 @@ pub struct Secret { /// Optional. Rotation policy attached to the Secret. May be excluded if there is no /// rotation policy. - pub rotation: Option, + pub rotation: , /// Optional. Mapping from version alias to version name. /// @@ -125,7 +125,7 @@ pub struct Secret { /// Version-Alias pairs will be viewable via GetSecret and modifiable via /// UpdateSecret. Access by alias is only be supported on /// GetSecretVersion and AccessSecretVersion. - pub version_aliases: Option>, + pub version_aliases: , /// Optional. Custom metadata about the secret. /// @@ -139,7 +139,7 @@ pub struct Secret { /// alphanumerics in between these symbols. /// /// The total size of annotation keys and values must be less than 16KiB. - pub annotations: Option>, + pub annotations: , /// Optional. Secret Version TTL after destruction request /// @@ -155,7 +155,7 @@ pub struct Secret { /// Updates to the Secret encryption configuration only apply to /// SecretVersions added afterwards. They do not apply /// retroactively to existing SecretVersions. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// A policy that defines the replication and encryption configuration of data. @@ -165,10 +165,10 @@ pub struct Secret { pub struct Replication { /// The Secret will automatically be replicated without any restrictions. - pub automatic: Option, + pub automatic: , /// The Secret will only be replicated into the locations specified. - pub user_managed: Option, + pub user_managed: , } /// A replication policy that replicates the Secret payload without any @@ -184,7 +184,7 @@ pub struct Automatic { /// Updates to the Secret encryption configuration only apply to /// SecretVersions added afterwards. They do not apply /// retroactively to existing SecretVersions. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// Configuration for encrypting secret payloads using customer-managed @@ -238,7 +238,7 @@ pub struct Replica { /// Updates to the Secret encryption configuration only apply to /// SecretVersions added afterwards. They do not apply /// retroactively to existing SecretVersions. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// A Pub/Sub topic which Secret Manager will publish to when control plane @@ -287,7 +287,7 @@ pub struct Rotation { pub struct AddSecretVersionRequest { /// Required. The secret payload of the SecretVersion. - pub payload: Option, + pub payload: , } /// A secret payload resource in the Secret Manager API. This contains the @@ -338,7 +338,7 @@ pub struct SecretVersion { pub state: Option, /// The replication status of the SecretVersion. - pub replication_status: Option, + pub replication_status: , /// Output only. Etag of the currently stored SecretVersion. pub etag: Option, @@ -358,7 +358,7 @@ pub struct SecretVersion { /// Output only. The customer-managed encryption status of the SecretVersion. Only /// populated if customer-managed encryption is used and Secret is /// a Regionalised Secret. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// The replication status of a SecretVersion. @@ -372,14 +372,14 @@ pub struct ReplicationStatus { /// /// Only populated if the parent Secret has an automatic replication /// policy. - pub automatic: Option, + pub automatic: , /// Describes the replication status of a SecretVersion with /// user-managed replication. /// /// Only populated if the parent Secret has a user-managed replication /// policy. - pub user_managed: Option, + pub user_managed: , } /// The replication status of a SecretVersion using automatic replication. @@ -393,7 +393,7 @@ pub struct AutomaticStatus { /// Output only. The customer-managed encryption status of the SecretVersion. Only /// populated if customer-managed encryption is used. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// Describes the status of customer-managed encryption. @@ -434,7 +434,7 @@ pub struct ReplicaStatus { /// Output only. The customer-managed encryption status of the SecretVersion. Only /// populated if customer-managed encryption is used. - pub customer_managed_encryption: Option, + pub customer_managed_encryption: , } /// A generic empty message that you can re-use to avoid defining duplicated @@ -481,7 +481,7 @@ pub struct AccessSecretVersionResponse { pub name: Option, /// Secret payload - pub payload: Option, + pub payload: , } /// Request message for SecretManagerService.DisableSecretVersion. @@ -530,7 +530,7 @@ pub struct SetIamPolicyRequest { /// the policy is limited to a few 10s of KB. An empty policy is a /// valid policy but certain Google Cloud services (such as Projects) /// might reject them. - pub policy: Option, + pub policy: , /// OPTIONAL: A FieldMask specifying which fields of the policy to modify. Only /// the fields in the mask will be modified. If no mask is provided, the @@ -783,7 +783,7 @@ pub struct Binding { /// To learn which resources support conditions in their IAM policies, see the /// [IAM /// documentation](https://cloud.google.com/iam/help/conditions/resource-policies). - pub condition: Option, + pub condition: , } /// Represents a textual expression in the Common Expression Language (CEL)