diff --git a/internal/librariangen/protoc/protoc.go b/internal/librariangen/protoc/protoc.go new file mode 100644 index 0000000000..b7bf0ad19e --- /dev/null +++ b/internal/librariangen/protoc/protoc.go @@ -0,0 +1,100 @@ +// Copyright 2025 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 +// +// http://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 protoc + +import ( + "fmt" + "os" + "path/filepath" + "strings" + +) + +// ConfigProvider is an interface that describes the configuration needed +// by the Build function. This allows the protoc package to be decoupled +// from the source of the configuration (e.g., Bazel files, JSON, etc.). +type ConfigProvider interface { + ServiceYAML() string + GapicYAML() string + GRPCServiceConfig() string + Transport() string + HasRESTNumericEnums() bool + HasGAPIC() bool +} + +// Build constructs the full protoc command arguments for a given API. +func Build(apiServiceDir string, config ConfigProvider, sourceDir, outputDir string) ([]string, error) { + // Gather all .proto files in the API's source directory. + entries, err := os.ReadDir(apiServiceDir) + if err != nil { + return nil, fmt.Errorf("librariangen: failed to read API source directory %s: %w", apiServiceDir, err) + } + + var protoFiles []string + for _, entry := range entries { + if !entry.IsDir() && filepath.Ext(entry.Name()) == ".proto" { + protoFiles = append(protoFiles, filepath.Join(apiServiceDir, entry.Name())) + } + } + + if len(protoFiles) == 0 { + return nil, fmt.Errorf("librariangen: no .proto files found in %s", apiServiceDir) + } + + // Construct the protoc command arguments. + var gapicOpts []string + if config.HasGAPIC() { + if config.ServiceYAML() != "" { + gapicOpts = append(gapicOpts, fmt.Sprintf("api-service-config=%s", filepath.Join(apiServiceDir, config.ServiceYAML()))) + } + if config.GapicYAML() != "" { + gapicOpts = append(gapicOpts, fmt.Sprintf("gapic-config=%s", filepath.Join(apiServiceDir, config.GapicYAML()))) + } + if config.GRPCServiceConfig() != "" { + gapicOpts = append(gapicOpts, fmt.Sprintf("grpc-service-config=%s", filepath.Join(apiServiceDir, config.GRPCServiceConfig()))) + } + if config.Transport() != "" { + gapicOpts = append(gapicOpts, fmt.Sprintf("transport=%s", config.Transport())) + } + if config.HasRESTNumericEnums() { + gapicOpts = append(gapicOpts, "rest-numeric-enums") + } + } + + args := []string{ + "protoc", + "--experimental_allow_proto3_optional", + } + + args = append(args, fmt.Sprintf("--java_out=%s", outputDir)) + if config.HasGAPIC() { + args = append(args, fmt.Sprintf("--java_gapic_out=metadata:%s", filepath.Join(outputDir, "java_gapic.zip"))) + + if len(gapicOpts) > 0 { + args = append(args, "--java_gapic_opt="+strings.Join(gapicOpts, ",")) + } + } + + args = append(args, + // The -I flag specifies the import path for protoc. All protos + // and their dependencies must be findable from this path. + // The /source mount contains the complete googleapis repository. + "-I="+sourceDir, + ) + + args = append(args, protoFiles...) + + return args, nil +} \ No newline at end of file diff --git a/internal/librariangen/protoc/protoc_test.go b/internal/librariangen/protoc/protoc_test.go new file mode 100644 index 0000000000..7e7f62aec6 --- /dev/null +++ b/internal/librariangen/protoc/protoc_test.go @@ -0,0 +1,137 @@ +// Copyright 2025 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 +// +// http://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 protoc + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +// mockConfigProvider is a mock implementation of the ConfigProvider interface for testing. +type mockConfigProvider struct { + serviceYAML string + gapicYAML string + grpcServiceConfig string + transport string + restNumericEnums bool + hasGAPIC bool +} + +func (m *mockConfigProvider) ServiceYAML() string { return m.serviceYAML } +func (m *mockConfigProvider) GapicYAML() string { return m.gapicYAML } +func (m *mockConfigProvider) GRPCServiceConfig() string { return m.grpcServiceConfig } +func (m *mockConfigProvider) Transport() string { return m.transport } +func (m *mockConfigProvider) HasRESTNumericEnums() bool { return m.restNumericEnums } +func (m *mockConfigProvider) HasGAPIC() bool { return m.hasGAPIC } + +func TestBuild(t *testing.T) { + // The testdata directory is a curated version of a valid protoc + // import path that contains all the necessary proto definitions. + sourceDir, err := filepath.Abs("../testdata/generate/source") + if err != nil { + t.Fatalf("failed to get absolute path for sourceDir: %v", err) + } + tests := []struct { + name string + apiPath string + config mockConfigProvider + want []string + }{ + { + name: "java_grpc_library rule", + apiPath: "google/cloud/workflows/v1", + config: mockConfigProvider{ + transport: "grpc", + grpcServiceConfig: "workflows_grpc_service_config.json", + gapicYAML: "workflows_gapic.yaml", + serviceYAML: "workflows_v1.yaml", + restNumericEnums: true, + hasGAPIC: true, + }, + want: []string{ + "protoc", + "--experimental_allow_proto3_optional", + "--java_out=/output", + "--java_gapic_out=metadata:/output/java_gapic.zip", + "--java_gapic_opt=" + strings.Join([]string{ + "api-service-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_v1.yaml"), + "gapic-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_gapic.yaml"), + "grpc-service-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_grpc_service_config.json"), + "transport=grpc", + "rest-numeric-enums", + }, ","), + "-I=" + sourceDir, + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows.proto"), + }, + }, + { + name: "java_proto_library rule with legacy gRPC", + apiPath: "google/cloud/secretmanager/v1beta2", + config: mockConfigProvider{ + transport: "grpc", + grpcServiceConfig: "secretmanager_grpc_service_config.json", + serviceYAML: "secretmanager_v1beta2.yaml", + restNumericEnums: true, + hasGAPIC: true, + }, + want: []string{ + "protoc", + "--experimental_allow_proto3_optional", + "--java_out=/output", + "--java_gapic_out=metadata:/output/java_gapic.zip", + "--java_gapic_opt=" + strings.Join([]string{ + "api-service-config=" + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager_v1beta2.yaml"), + "grpc-service-config=" + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager_grpc_service_config.json"), + "transport=grpc", + "rest-numeric-enums", + }, ","), + "-I=" + sourceDir, + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager.proto"), + }, + }, + { + // Note: we don't have a separate test directory with a proto-only library; + // the config is used to say "don't generate GAPIC". + name: "proto-only", + apiPath: "google/cloud/secretmanager/v1beta2", + config: mockConfigProvider{ + hasGAPIC: false, + }, + want: []string{ + "protoc", + "--experimental_allow_proto3_optional", + "--java_out=/output", + "-I=" + sourceDir, + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager.proto"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Build(filepath.Join(sourceDir, tt.apiPath), &tt.config, sourceDir, "/output") + if err != nil { + t.Fatalf("Build() failed: %v", err) + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("Build() mismatch (-want +got):\n%s", diff) + } + }) + } +} \ No newline at end of file diff --git a/internal/librariangen/testdata/generate/source/google/api/annotations.proto b/internal/librariangen/testdata/generate/source/google/api/annotations.proto new file mode 100644 index 0000000000..c749d43d6f --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/api/annotations.proto @@ -0,0 +1,46 @@ +// Copyright 2025 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 +// +// http://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. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; + +extend google.protobuf.MethodOptions { + HttpRule http = 72295728; +} + +message HttpRule { + string selector = 1; + oneof pattern { + string get = 2; + string put = 3; + string post = 4; + string delete = 5; + string patch = 6; + CustomHttpPattern custom = 8; + } + string body = 7; + repeated HttpRule additional_bindings = 11; +} + +message CustomHttpPattern { + string kind = 1; + string path = 2; +} diff --git a/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/BUILD.bazel b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/BUILD.bazel new file mode 100644 index 0000000000..a010cb6af5 --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/BUILD.bazel @@ -0,0 +1,53 @@ +java_proto_library( + name = "secretmanager_java_proto", + deps = [":secretmanager_proto"], +) + +java_grpc_library( + name = "secretmanager_java_grpc", + srcs = [":secretmanager_proto"], + deps = [":secretmanager_java_proto"], +) + +java_gapic_library( + name = "secretmanager_java_gapic", + srcs = [":secretmanager_proto_with_info"], + gapic_yaml = None, + grpc_service_config = "secretmanager_grpc_service_config.json", + rest_numeric_enums = True, + service_yaml = "secretmanager_v1beta2.yaml", + test_deps = [ + "//google/cloud/location:location_java_grpc", + "//google/iam/v1:iam_java_grpc", + ":secretmanager_java_grpc", + ], + transport = "grpc+rest", + deps = [ + ":secretmanager_java_proto", + "//google/api:api_java_proto", + "//google/cloud/location:location_java_proto", + "//google/iam/v1:iam_java_proto", + ], +) + +java_gapic_test( + name = "secretmanager_java_gapic_test_suite", + test_classes = [ + "com.google.cloud.secretmanager.v1beta2.SecretManagerServiceClientHttpJsonTest", + "com.google.cloud.secretmanager.v1beta2.SecretManagerServiceClientTest", + ], + runtime_deps = [":secretmanager_java_gapic_test"], +) + +# Open Source Packages +java_gapic_assembly_gradle_pkg( + name = "google-cloud-secretmanager-v1beta2-java", + transport = "grpc+rest", + deps = [ + ":secretmanager_java_gapic", + ":secretmanager_java_grpc", + ":secretmanager_java_proto", + ":secretmanager_proto", + ], + include_samples = True, +) \ No newline at end of file diff --git a/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager.proto b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager.proto new file mode 100644 index 0000000000..a3d1df63be --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager.proto @@ -0,0 +1,48 @@ +// Copyright 2025 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 +// +// http://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. + +syntax = "proto3"; + +package google.cloud.secretmanager.v1beta2; + +import "google/api/annotations.proto"; + +option java_multiple_files = true; +option java_outer_classname = "ServiceProto"; +option java_package = "com.google.cloud.secretmanager.v1beta2"; + +// A Secret is a secret value. +message Secret { + string name = 1; +} + +// Request for the `ListSecrets` method. +message ListSecretsRequest { + string parent = 1; +} + +// Response for the `ListSecrets` method. +message ListSecretsResponse { + repeated Secret Secrets = 1; +} + +// Service for managing secrets. +service Secrets { + // Lists Secrets in a given project and location. + rpc ListSecrets(ListSecretsRequest) returns (ListSecretsResponse) { + option (google.api.http) = { + get: "/v1/{parent=projects/*/locations/*}/Secrets" + }; + } +} diff --git a/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager_v1beta2.yaml b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager_v1beta2.yaml new file mode 100644 index 0000000000..a273af4d01 --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/secretmanager/v1beta2/secretmanager_v1beta2.yaml @@ -0,0 +1,11 @@ +type: google.api.Service +config_version: 3 +name: secretmanager.googleapis.com +title: Secret Manager API + +apis: +- name: google.cloud.secretmanager.v1beta2.Secrets + +documentation: + summary: |- + Subset of real Secret Manager API. diff --git a/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/BUILD.bazel b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/BUILD.bazel new file mode 100644 index 0000000000..e3c9066a69 --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/BUILD.bazel @@ -0,0 +1,50 @@ +java_proto_library( + name = "workflows_java_proto", + deps = [":workflows_proto"], +) + +java_grpc_library( + name = "workflows_java_grpc", + srcs = [":workflows_proto"], + deps = [":workflows_java_proto"], +) + +java_gapic_library( + name = "workflows_java_gapic", + srcs = [":workflows_proto_with_info"], + grpc_service_config = "workflows_grpc_service_config.json", + rest_numeric_enums = True, + service_yaml = "workflows_v1.yaml", + test_deps = [ + "//google/cloud/location:location_java_grpc", + ":workflows_java_grpc", + ], + transport = "grpc+rest", + deps = [ + ":workflows_java_proto", + "//google/api:api_java_proto", + "//google/cloud/location:location_java_proto", + ], +) + +java_gapic_test( + name = "workflows_java_gapic_test_suite", + test_classes = [ + "com.google.cloud.workflows.v1.WorkflowsClientHttpJsonTest", + "com.google.cloud.workflows.v1.WorkflowsClientTest", + ], + runtime_deps = [":workflows_java_gapic_test"], +) + +# Open Source Packages +java_gapic_assembly_gradle_pkg( + name = "google-cloud-workflows-v1-java", + include_samples = True, + transport = "grpc+rest", + deps = [ + ":workflows_java_gapic", + ":workflows_java_grpc", + ":workflows_java_proto", + ":workflows_proto", + ], +) \ No newline at end of file diff --git a/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows.proto b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows.proto new file mode 100644 index 0000000000..a65843a154 --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows.proto @@ -0,0 +1,48 @@ +// Copyright 2025 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 +// +// http://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. + +syntax = "proto3"; + +package google.cloud.workflows.v1; + +import "google/api/annotations.proto"; + +option java_multiple_files = true; +option java_outer_classname = "WorkflowsProto"; +option java_package = "com.google.cloud.workflows.v1"; + +// A workflow is a collection of steps that are executed in a predefined order. +message Workflow { + string name = 1; +} + +// Request for the `ListWorkflows` method. +message ListWorkflowsRequest { + string parent = 1; +} + +// Response for the `ListWorkflows` method. +message ListWorkflowsResponse { + repeated Workflow workflows = 1; +} + +// Service for managing workflows. +service Workflows { + // Lists workflows in a given project and location. + rpc ListWorkflows(ListWorkflowsRequest) returns (ListWorkflowsResponse) { + option (google.api.http) = { + get: "/v1/{parent=projects/*/locations/*}/workflows" + }; + } +} diff --git a/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows_v1.yaml b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows_v1.yaml new file mode 100644 index 0000000000..76eae7cd3c --- /dev/null +++ b/internal/librariangen/testdata/generate/source/google/cloud/workflows/v1/workflows_v1.yaml @@ -0,0 +1,56 @@ +type: google.api.Service +config_version: 3 +name: workflows.googleapis.com +title: Workflows API + +apis: +- name: google.cloud.location.Locations +- name: google.cloud.workflows.v1.Workflows +- name: google.longrunning.Operations + +types: +- name: google.cloud.workflows.v1.OperationMetadata + +documentation: + summary: |- + Manage workflow definitions. To execute workflows and manage executions, + see the Workflows Executions API. + rules: + - 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. + +http: + rules: + - selector: google.cloud.location.Locations.GetLocation + get: '/v1/{name=projects/*/locations/*}' + - selector: google.cloud.location.Locations.ListLocations + get: '/v1/{name=projects/*}/locations' + - selector: google.longrunning.Operations.DeleteOperation + delete: '/v1/{name=projects/*/locations/*/operations/*}' + - selector: google.longrunning.Operations.GetOperation + get: '/v1/{name=projects/*/locations/*/operations/*}' + - selector: google.longrunning.Operations.ListOperations + get: '/v1/{name=projects/*/locations/*}/operations' + +authentication: + rules: + - selector: google.cloud.location.Locations.GetLocation + oauth: + canonical_scopes: |- + https://www.googleapis.com/auth/cloud-platform + - selector: google.cloud.location.Locations.ListLocations + oauth: + canonical_scopes: |- + https://www.googleapis.com/auth/cloud-platform + - selector: 'google.cloud.workflows.v1.Workflows.*' + oauth: + canonical_scopes: |- + https://www.googleapis.com/auth/cloud-platform + - selector: 'google.longrunning.Operations.*' + oauth: + canonical_scopes: |- + https://www.googleapis.com/auth/cloud-platform +