Skip to content

Commit feabef3

Browse files
authored
feat(librariangen): add bazel package (#3940)
Based on https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen/bazel with adaptation for Java.
1 parent 8d6c1f9 commit feabef3

File tree

2 files changed

+374
-0
lines changed

2 files changed

+374
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 bazel
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"log/slog"
21+
"os"
22+
"path/filepath"
23+
"regexp"
24+
"strconv"
25+
"strings"
26+
"sync"
27+
)
28+
29+
// Config holds configuration extracted from a googleapis BUILD.bazel file.
30+
type Config struct {
31+
grpcServiceConfig string
32+
restNumericEnums bool
33+
serviceYAML string
34+
transport string
35+
hasGAPIC bool
36+
}
37+
38+
// HasGAPIC indicates whether the GAPIC generator should be run.
39+
func (c *Config) HasGAPIC() bool { return c.hasGAPIC }
40+
41+
// ServiceYAML is the client config file in the API version directory in googleapis.
42+
func (c *Config) ServiceYAML() string { return c.serviceYAML }
43+
44+
// GRPCServiceConfig is name of the gRPC service config JSON file.
45+
func (c *Config) GRPCServiceConfig() string { return c.grpcServiceConfig }
46+
47+
// Transport is typically one of "grpc", "rest" or "grpc+rest".
48+
func (c *Config) Transport() string { return c.transport }
49+
50+
// HasRESTNumericEnums indicates whether the generated client should support numeric enums.
51+
func (c *Config) HasRESTNumericEnums() bool { return c.restNumericEnums }
52+
53+
// Validate ensures that the configuration is valid.
54+
func (c *Config) Validate() error {
55+
if c.hasGAPIC {
56+
if c.serviceYAML == "" {
57+
return errors.New("librariangen: serviceYAML is not set")
58+
}
59+
}
60+
return nil
61+
}
62+
63+
var javaGapicLibraryRE = regexp.MustCompile(`java_gapic_library\((?s:.)*?\)`)
64+
// Parse reads a BUILD.bazel file from the given directory and extracts the
65+
// relevant configuration from the java_gapic_library rule.
66+
func Parse(dir string) (*Config, error) {
67+
c := &Config{}
68+
fp := filepath.Join(dir, "BUILD.bazel")
69+
data, err := os.ReadFile(fp)
70+
if err != nil {
71+
return nil, fmt.Errorf("librariangen: failed to read BUILD.bazel file %s: %w", fp, err)
72+
}
73+
content := string(data)
74+
75+
gapicLibraryBlock := javaGapicLibraryRE.FindString(content)
76+
if gapicLibraryBlock != "" {
77+
c.hasGAPIC = true
78+
c.grpcServiceConfig = findString(gapicLibraryBlock, "grpc_service_config")
79+
c.serviceYAML = strings.TrimPrefix(findString(gapicLibraryBlock, "service_yaml"), ":")
80+
c.transport = findString(gapicLibraryBlock, "transport")
81+
if c.restNumericEnums, err = findBool(gapicLibraryBlock, "rest_numeric_enums"); err != nil {
82+
return nil, fmt.Errorf("librariangen: failed to parse BUILD.bazel file %s: %w", fp, err)
83+
}
84+
}
85+
if err := c.Validate(); err != nil {
86+
return nil, fmt.Errorf("librariangen: invalid bazel config in %s: %w", dir, err)
87+
}
88+
slog.Debug("librariangen: bazel config loaded", "conf", c)
89+
return c, nil
90+
}
91+
92+
var reCache = &sync.Map{}
93+
94+
func getRegexp(key, pattern string) *regexp.Regexp {
95+
val, ok := reCache.Load(key)
96+
if !ok {
97+
val = regexp.MustCompile(pattern)
98+
reCache.Store(key, val)
99+
}
100+
return val.(*regexp.Regexp)
101+
}
102+
103+
func findString(content, name string) string {
104+
re := getRegexp("findString_"+name, fmt.Sprintf(`%s\s*=\s*(?:"([^"]+)"|'([^']+)'){1}`, name))
105+
match := re.FindStringSubmatch(content)
106+
if len(match) > 2 {
107+
if match[1] != "" {
108+
return match[1] // Double-quoted
109+
}
110+
return match[2] // Single-quoted
111+
}
112+
slog.Debug("librariangen: failed to find string attr in BUILD.bazel", "name", name)
113+
return ""
114+
}
115+
116+
func findBool(content, name string) (bool, error) {
117+
re := getRegexp("findBool_"+name, fmt.Sprintf(`%s\s*=\s*(\w+)`, name))
118+
if match := re.FindStringSubmatch(content); len(match) > 1 {
119+
if b, err := strconv.ParseBool(match[1]); err == nil {
120+
return b, nil
121+
}
122+
return false, fmt.Errorf("librariangen: failed to parse bool attr in BUILD.bazel: %q, got: %q", name, match[1])
123+
}
124+
slog.Debug("librariangen: failed to find bool attr in BUILD.bazel", "name", name)
125+
return false, nil
126+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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 bazel
16+
17+
import (
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
)
22+
23+
func TestParse(t *testing.T) {
24+
content := `
25+
java_grpc_library(
26+
name = "asset_java_grpc",
27+
srcs = [":asset_proto"],
28+
deps = [":asset_java_proto"],
29+
)
30+
31+
java_gapic_library(
32+
name = "asset_java_gapic",
33+
srcs = [":asset_proto_with_info"],
34+
grpc_service_config = "cloudasset_grpc_service_config.json",
35+
rest_numeric_enums = True,
36+
service_yaml = "cloudasset_v1.yaml",
37+
test_deps = [
38+
":asset_java_grpc",
39+
"//google/iam/v1:iam_java_grpc",
40+
],
41+
transport = 'grpc+rest',
42+
deps = [
43+
":asset_java_proto",
44+
"//google/api:api_java_proto",
45+
"//google/iam/v1:iam_java_proto",
46+
],
47+
)
48+
`
49+
tmpDir := t.TempDir()
50+
buildPath := filepath.Join(tmpDir, "BUILD.bazel")
51+
if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil {
52+
t.Fatalf("failed to write test file: %v", err)
53+
}
54+
55+
got, err := Parse(tmpDir)
56+
if err != nil {
57+
t.Fatalf("Parse() failed: %v", err)
58+
}
59+
60+
t.Run("HasGAPIC", func(t *testing.T) {
61+
if !got.HasGAPIC() {
62+
t.Error("HasGAPIC() = false; want true")
63+
}
64+
})
65+
t.Run("ServiceYAML", func(t *testing.T) {
66+
if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want {
67+
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
68+
}
69+
})
70+
t.Run("GRPCServiceConfig", func(t *testing.T) {
71+
if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want {
72+
t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want)
73+
}
74+
})
75+
t.Run("Transport", func(t *testing.T) {
76+
if want := "grpc+rest"; got.Transport() != want {
77+
t.Errorf("Transport() = %q; want %q", got.Transport(), want)
78+
}
79+
})
80+
t.Run("HasRESTNumericEnums", func(t *testing.T) {
81+
if !got.HasRESTNumericEnums() {
82+
t.Error("HasRESTNumericEnums() = false; want true")
83+
}
84+
})
85+
}
86+
87+
func TestParse_serviceConfigIsTarget(t *testing.T) {
88+
content := `
89+
java_grpc_library(
90+
name = "asset_java_grpc",
91+
srcs = [":asset_proto"],
92+
deps = [":asset_java_proto"],
93+
)
94+
95+
java_gapic_library(
96+
name = "asset_java_gapic",
97+
srcs = [":asset_proto_with_info"],
98+
grpc_service_config = "cloudasset_grpc_service_config.json",
99+
rest_numeric_enums = True,
100+
service_yaml = ":cloudasset_v1.yaml",
101+
test_deps = [
102+
":asset_java_grpc",
103+
"//google/iam/v1:iam_java_grpc",
104+
],
105+
transport = "grpc+rest",
106+
deps = [
107+
":asset_java_proto",
108+
"//google/api:api_java_proto",
109+
"//google/iam/v1:iam_java_proto",
110+
],
111+
)
112+
`
113+
tmpDir := t.TempDir()
114+
buildPath := filepath.Join(tmpDir, "BUILD.bazel")
115+
if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil {
116+
t.Fatalf("failed to write test file: %v", err)
117+
}
118+
119+
got, err := Parse(tmpDir)
120+
if err != nil {
121+
t.Fatalf("Parse() failed: %v", err)
122+
}
123+
124+
if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want {
125+
t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want)
126+
}
127+
}
128+
129+
func TestConfig_Validate(t *testing.T) {
130+
tests := []struct {
131+
name string
132+
cfg *Config
133+
wantErr bool
134+
}{
135+
{
136+
name: "valid GAPIC",
137+
cfg: &Config{
138+
hasGAPIC: true,
139+
serviceYAML: "b",
140+
grpcServiceConfig: "c",
141+
transport: "d",
142+
},
143+
wantErr: false,
144+
},
145+
{
146+
name: "valid non-GAPIC",
147+
cfg: &Config{},
148+
wantErr: false,
149+
},
150+
{
151+
name: "gRPC service config and transport are optional",
152+
cfg: &Config{hasGAPIC: true, serviceYAML: "b"},
153+
wantErr: false,
154+
},
155+
{
156+
name: "missing serviceYAML",
157+
cfg: &Config{hasGAPIC: true, grpcServiceConfig: "c", transport: "d"},
158+
wantErr: true,
159+
},
160+
}
161+
for _, tt := range tests {
162+
t.Run(tt.name, func(t *testing.T) {
163+
if err := tt.cfg.Validate(); (err != nil) != tt.wantErr {
164+
t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr)
165+
}
166+
})
167+
}
168+
}
169+
170+
func TestParse_noGapic(t *testing.T) {
171+
content := `
172+
java_grpc_library(
173+
name = "asset_java_grpc",
174+
srcs = [":asset_proto"],
175+
deps = [":asset_java_proto"],
176+
)
177+
`
178+
tmpDir := t.TempDir()
179+
buildPath := filepath.Join(tmpDir, "BUILD.bazel")
180+
if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil {
181+
t.Fatalf("failed to write test file: %v", err)
182+
}
183+
184+
got, err := Parse(tmpDir)
185+
if err != nil {
186+
t.Fatalf("Parse() failed: %v", err)
187+
}
188+
189+
if got.HasGAPIC() {
190+
t.Error("HasGAPIC() = true; want false")
191+
}
192+
}
193+
194+
func TestParse_missingSomeAttrs(t *testing.T) {
195+
content := `
196+
java_gapic_library(
197+
name = "asset_java_gapic",
198+
service_yaml = "cloudasset_v1.yaml",
199+
)
200+
`
201+
tmpDir := t.TempDir()
202+
buildPath := filepath.Join(tmpDir, "BUILD.bazel")
203+
if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil {
204+
t.Fatalf("failed to write test file: %v", err)
205+
}
206+
207+
got, err := Parse(tmpDir)
208+
if err != nil {
209+
t.Fatalf("Parse() failed: %v", err)
210+
}
211+
212+
if got.GRPCServiceConfig() != "" {
213+
t.Errorf("GRPCServiceConfig() = %q; want \"\"", got.GRPCServiceConfig())
214+
}
215+
if got.Transport() != "" {
216+
t.Errorf("Transport() = %q; want \"\"", got.Transport())
217+
}
218+
if got.HasRESTNumericEnums() {
219+
t.Error("HasRESTNumericEnums() = true; want false")
220+
}
221+
}
222+
223+
func TestParse_invalidBoolAttr(t *testing.T) {
224+
content := `
225+
java_gapic_library(
226+
name = "asset_java_gapic",
227+
rest_numeric_enums = "not-a-bool",
228+
)
229+
`
230+
tmpDir := t.TempDir()
231+
buildPath := filepath.Join(tmpDir, "BUILD.bazel")
232+
if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil {
233+
t.Fatalf("failed to write test file: %v", err)
234+
}
235+
236+
_, err := Parse(tmpDir)
237+
if err == nil {
238+
t.Error("Parse() succeeded; want error")
239+
}
240+
}
241+
242+
func TestParse_noBuildFile(t *testing.T) {
243+
tmpDir := t.TempDir()
244+
_, err := Parse(tmpDir)
245+
if err == nil {
246+
t.Error("Parse() succeeded; want error")
247+
}
248+
}

0 commit comments

Comments
 (0)