Skip to content

Commit 42b04c9

Browse files
committed
feat(librariangen): add protoc package
This will be used for constructing the protoc command for generating Java GAPICs. Based on https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen/protoc with the following important changes: * The `Build` function in `protoc.go` is updated to construct the `protoc` command with the correct arguments for Java GAPIC generation. * The tests in `protoc_test.go` are updated to reflect the changes in `protoc.go`. * The `gapicImportPath` is removed from the test configuration, as it is not relevant for Java.'
1 parent fe44aed commit 42b04c9

10 files changed

Lines changed: 585 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package protoc
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"cloud.google.com/java/internal/librariangen/request"
24+
)
25+
26+
// ConfigProvider is an interface that describes the configuration needed
27+
// by the Build function. This allows the protoc package to be decoupled
28+
// from the source of the configuration (e.g., Bazel files, JSON, etc.).
29+
type ConfigProvider interface {
30+
ServiceYAML() string
31+
GapicYAML() string
32+
GRPCServiceConfig() string
33+
Transport() string
34+
HasRESTNumericEnums() bool
35+
HasGAPIC() bool
36+
}
37+
38+
// Build constructs the full protoc command arguments for a given API.
39+
func Build(lib *request.Library, api *request.API, apiServiceDir string, config ConfigProvider, sourceDir, outputDir string) ([]string, error) {
40+
// Gather all .proto files in the API's source directory.
41+
entries, err := os.ReadDir(apiServiceDir)
42+
if err != nil {
43+
return nil, fmt.Errorf("librariangen: failed to read API source directory %s: %w", apiServiceDir, err)
44+
}
45+
46+
var protoFiles []string
47+
for _, entry := range entries {
48+
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".proto" {
49+
protoFiles = append(protoFiles, filepath.Join(apiServiceDir, entry.Name()))
50+
}
51+
}
52+
53+
if len(protoFiles) == 0 {
54+
return nil, fmt.Errorf("librariangen: no .proto files found in %s", apiServiceDir)
55+
}
56+
57+
// Construct the protoc command arguments.
58+
var gapicOpts []string
59+
if config.HasGAPIC() {
60+
if config.ServiceYAML() != "" {
61+
gapicOpts = append(gapicOpts, fmt.Sprintf("api-service-config=%s", filepath.Join(apiServiceDir, config.ServiceYAML())))
62+
}
63+
if config.GapicYAML() != "" {
64+
gapicOpts = append(gapicOpts, fmt.Sprintf("gapic-config=%s", filepath.Join(apiServiceDir, config.GapicYAML())))
65+
}
66+
if config.GRPCServiceConfig() != "" {
67+
gapicOpts = append(gapicOpts, fmt.Sprintf("grpc-service-config=%s", filepath.Join(apiServiceDir, config.GRPCServiceConfig())))
68+
}
69+
if config.Transport() != "" {
70+
gapicOpts = append(gapicOpts, fmt.Sprintf("transport=%s", config.Transport()))
71+
}
72+
if config.HasRESTNumericEnums() {
73+
gapicOpts = append(gapicOpts, "rest-numeric-enums")
74+
}
75+
}
76+
77+
args := []string{
78+
"protoc",
79+
"--experimental_allow_proto3_optional",
80+
}
81+
82+
args = append(args, fmt.Sprintf("--java_out=%s", outputDir))
83+
if config.HasGAPIC() {
84+
args = append(args, fmt.Sprintf("--java_gapic_out=metadata:%s", filepath.Join(outputDir, "java_gapic.zip")))
85+
86+
if len(gapicOpts) > 0 {
87+
args = append(args, "--java_gapic_opt="+strings.Join(gapicOpts, ","))
88+
}
89+
}
90+
91+
args = append(args,
92+
// The -I flag specifies the import path for protoc. All protos
93+
// and their dependencies must be findable from this path.
94+
// The /source mount contains the complete googleapis repository.
95+
"-I="+sourceDir,
96+
)
97+
98+
args = append(args, protoFiles...)
99+
100+
return args, nil
101+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package protoc
16+
17+
import (
18+
"path/filepath"
19+
"strings"
20+
"testing"
21+
22+
"cloud.google.com/java/internal/librariangen/request"
23+
"github.com/google/go-cmp/cmp"
24+
)
25+
26+
// mockConfigProvider is a mock implementation of the ConfigProvider interface for testing.
27+
type mockConfigProvider struct {
28+
serviceYAML string
29+
gapicYAML string
30+
grpcServiceConfig string
31+
transport string
32+
restNumericEnums bool
33+
hasGAPIC bool
34+
}
35+
36+
func (m *mockConfigProvider) ServiceYAML() string { return m.serviceYAML }
37+
func (m *mockConfigProvider) GapicYAML() string { return m.gapicYAML }
38+
func (m *mockConfigProvider) GRPCServiceConfig() string { return m.grpcServiceConfig }
39+
func (m *mockConfigProvider) Transport() string { return m.transport }
40+
func (m *mockConfigProvider) HasRESTNumericEnums() bool { return m.restNumericEnums }
41+
func (m *mockConfigProvider) HasGAPIC() bool { return m.hasGAPIC }
42+
43+
func TestBuild(t *testing.T) {
44+
// The testdata directory is a curated version of a valid protoc
45+
// import path that contains all the necessary proto definitions.
46+
sourceDir, err := filepath.Abs("../testdata/generate/source")
47+
if err != nil {
48+
t.Fatalf("failed to get absolute path for sourceDir: %v", err)
49+
}
50+
tests := []struct {
51+
name string
52+
apiPath string
53+
apiServiceDir string
54+
reqID string
55+
config mockConfigProvider
56+
want []string
57+
}{
58+
{
59+
name: "java_grpc_library rule",
60+
apiPath: "google/cloud/workflows/v1",
61+
reqID: "workflows",
62+
config: mockConfigProvider{
63+
transport: "grpc",
64+
grpcServiceConfig: "workflows_grpc_service_config.json",
65+
gapicYAML: "workflows_gapic.yaml",
66+
serviceYAML: "workflows_v1.yaml",
67+
restNumericEnums: true,
68+
hasGAPIC: true,
69+
},
70+
want: []string{
71+
"protoc",
72+
"--experimental_allow_proto3_optional",
73+
"--java_out=/output",
74+
"--java_gapic_out=metadata:/output/java_gapic.zip",
75+
"--java_gapic_opt=" + strings.Join([]string{
76+
"api-service-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_v1.yaml"),
77+
"gapic-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_gapic.yaml"),
78+
"grpc-service-config=" + filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows_grpc_service_config.json"),
79+
"transport=grpc",
80+
"rest-numeric-enums",
81+
}, ","),
82+
"-I=" + sourceDir,
83+
filepath.Join(sourceDir, "google/cloud/workflows/v1/workflows.proto"),
84+
},
85+
},
86+
{
87+
name: "java_proto_library rule with legacy gRPC",
88+
apiPath: "google/cloud/secretmanager/v1beta2",
89+
reqID: "secretmanager",
90+
config: mockConfigProvider{
91+
transport: "grpc",
92+
grpcServiceConfig: "secretmanager_grpc_service_config.json",
93+
serviceYAML: "secretmanager_v1beta2.yaml",
94+
restNumericEnums: true,
95+
hasGAPIC: true,
96+
},
97+
want: []string{
98+
"protoc",
99+
"--experimental_allow_proto3_optional",
100+
"--java_out=/output",
101+
"--java_gapic_out=metadata:/output/java_gapic.zip",
102+
"--java_gapic_opt=" + strings.Join([]string{
103+
"api-service-config=" + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager_v1beta2.yaml"),
104+
"grpc-service-config=" + filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager_grpc_service_config.json"),
105+
"transport=grpc",
106+
"rest-numeric-enums",
107+
}, ","),
108+
"-I=" + sourceDir,
109+
filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager.proto"),
110+
},
111+
},
112+
{
113+
// Note: we don't have a separate test directory with a proto-only library;
114+
// the config is used to say "don't generate GAPIC".
115+
name: "proto-only",
116+
apiPath: "google/cloud/secretmanager/v1beta2",
117+
reqID: "secretmanager",
118+
config: mockConfigProvider{
119+
hasGAPIC: false,
120+
},
121+
want: []string{
122+
"protoc",
123+
"--experimental_allow_proto3_optional",
124+
"--java_out=/output",
125+
"-I=" + sourceDir,
126+
filepath.Join(sourceDir, "google/cloud/secretmanager/v1beta2/secretmanager.proto"),
127+
},
128+
},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
req := &request.Library{
134+
ID: tt.reqID,
135+
}
136+
api := &request.API{
137+
Path: tt.apiPath,
138+
}
139+
140+
got, err := Build(req, api, filepath.Join(sourceDir, tt.apiPath), &tt.config, sourceDir, "/output")
141+
if err != nil {
142+
t.Fatalf("Build() failed: %v", err)
143+
}
144+
145+
if diff := cmp.Diff(tt.want, got); diff != "" {
146+
t.Errorf("Build() mismatch (-want +got):\n%s", diff)
147+
}
148+
})
149+
}
150+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "chronicle",
3+
"version": "0.1.1",
4+
"apis": [
5+
{
6+
"path": "google/cloud/chronicle/v1",
7+
"service_config": "chronicle_v1.yaml"
8+
}
9+
],
10+
"source_roots": [
11+
"chronicle",
12+
"internal/generated/snippets/chronicle"
13+
],
14+
"preserve_regex": [
15+
"chronicle/aliasshim/aliasshim.go",
16+
"chronicle/CHANGES.md"
17+
],
18+
"remove_regex": [
19+
"chronicle",
20+
"internal/generated/snippets/chronicle"
21+
]
22+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
package google.api;
18+
19+
import "google/protobuf/descriptor.proto";
20+
21+
option java_multiple_files = true;
22+
option java_outer_classname = "AnnotationsProto";
23+
option java_package = "com.google.api";
24+
25+
extend google.protobuf.MethodOptions {
26+
HttpRule http = 72295728;
27+
}
28+
29+
message HttpRule {
30+
string selector = 1;
31+
oneof pattern {
32+
string get = 2;
33+
string put = 3;
34+
string post = 4;
35+
string delete = 5;
36+
string patch = 6;
37+
CustomHttpPattern custom = 8;
38+
}
39+
string body = 7;
40+
repeated HttpRule additional_bindings = 11;
41+
}
42+
43+
message CustomHttpPattern {
44+
string kind = 1;
45+
string path = 2;
46+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
java_proto_library(
2+
name = "secretmanager_java_proto",
3+
deps = [":secretmanager_proto"],
4+
)
5+
6+
java_grpc_library(
7+
name = "secretmanager_java_grpc",
8+
srcs = [":secretmanager_proto"],
9+
deps = [":secretmanager_java_proto"],
10+
)
11+
12+
java_gapic_library(
13+
name = "secretmanager_java_gapic",
14+
srcs = [":secretmanager_proto_with_info"],
15+
gapic_yaml = None,
16+
grpc_service_config = "secretmanager_grpc_service_config.json",
17+
rest_numeric_enums = True,
18+
service_yaml = "secretmanager_v1beta2.yaml",
19+
test_deps = [
20+
"//google/cloud/location:location_java_grpc",
21+
"//google/iam/v1:iam_java_grpc",
22+
":secretmanager_java_grpc",
23+
],
24+
transport = "grpc+rest",
25+
deps = [
26+
":secretmanager_java_proto",
27+
"//google/api:api_java_proto",
28+
"//google/cloud/location:location_java_proto",
29+
"//google/iam/v1:iam_java_proto",
30+
],
31+
)
32+
33+
java_gapic_test(
34+
name = "secretmanager_java_gapic_test_suite",
35+
test_classes = [
36+
"com.google.cloud.secretmanager.v1beta2.SecretManagerServiceClientHttpJsonTest",
37+
"com.google.cloud.secretmanager.v1beta2.SecretManagerServiceClientTest",
38+
],
39+
runtime_deps = [":secretmanager_java_gapic_test"],
40+
)
41+
42+
# Open Source Packages
43+
java_gapic_assembly_gradle_pkg(
44+
name = "google-cloud-secretmanager-v1beta2-java",
45+
transport = "grpc+rest",
46+
deps = [
47+
":secretmanager_java_gapic",
48+
":secretmanager_java_grpc",
49+
":secretmanager_java_proto",
50+
":secretmanager_proto",
51+
],
52+
include_samples = True,
53+
)

0 commit comments

Comments
 (0)