From 971fbd691261b36f25e2606bdf043cef4cd12a10 Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Wed, 15 Oct 2025 03:28:16 +0000 Subject: [PATCH 1/4] feat(librariangen): add bazel package Based on https://github.com/googleapis/google-cloud-go/tree/main/internal/librariangen/bazel with adaptation for Java. --- internal/librariangen/bazel/parser.go | 110 +++++++++++++ internal/librariangen/bazel/parser_test.go | 182 +++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 internal/librariangen/bazel/parser.go create mode 100644 internal/librariangen/bazel/parser_test.go diff --git a/internal/librariangen/bazel/parser.go b/internal/librariangen/bazel/parser.go new file mode 100644 index 0000000000..321cbed76c --- /dev/null +++ b/internal/librariangen/bazel/parser.go @@ -0,0 +1,110 @@ +// 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 bazel + +import ( + "errors" + "fmt" + "log/slog" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// Config holds configuration extracted from a googleapis BUILD.bazel file. +type Config struct { + grpcServiceConfig string + restNumericEnums bool + serviceYAML string + transport string + hasGAPIC bool +} + +// HasGAPIC indicates whether the GAPIC generator should be run. +func (c *Config) HasGAPIC() bool { return c.hasGAPIC } + +// ServiceYAML is the client config file in the API version directory in googleapis. +func (c *Config) ServiceYAML() string { return c.serviceYAML } + +// GRPCServiceConfig is name of the gRPC service config JSON file. +func (c *Config) GRPCServiceConfig() string { return c.grpcServiceConfig } + +// Transport is typically one of "grpc", "rest" or "grpc+rest". +func (c *Config) Transport() string { return c.transport } + +// HasRESTNumericEnums indicates whether the generated client should support numeric enums. +func (c *Config) HasRESTNumericEnums() bool { return c.restNumericEnums } + +// Validate ensures that the configuration is valid. +func (c *Config) Validate() error { + if c.hasGAPIC { + if c.serviceYAML == "" { + return errors.New("librariangen: serviceYAML is not set") + } + } + return nil +} + +// Parse reads a BUILD.bazel file from the given directory and extracts the +// relevant configuration from the java_gapic_library rule. +func Parse(dir string) (*Config, error) { + c := &Config{} + fp := filepath.Join(dir, "BUILD.bazel") + data, err := os.ReadFile(fp) + if err != nil { + return nil, fmt.Errorf("librariangen: failed to read BUILD.bazel file %s: %w", fp, err) + } + content := string(data) + + re := regexp.MustCompile(`java_gapic_library\((?s:.)*?\)`) + gapicLibraryBlock := re.FindString(content) + if gapicLibraryBlock != "" { + c.hasGAPIC = true + c.grpcServiceConfig = findString(gapicLibraryBlock, "grpc_service_config") + c.serviceYAML = strings.TrimPrefix(findString(gapicLibraryBlock, "service_yaml"), ":") + c.transport = findString(gapicLibraryBlock, "transport") + if c.restNumericEnums, err = findBool(gapicLibraryBlock, "rest_numeric_enums"); err != nil { + return nil, fmt.Errorf("librariangen: failed to parse BUILD.bazel file %s: %w", fp, err) + } + } + if err := c.Validate(); err != nil { + return nil, fmt.Errorf("librariangen: invalid bazel config in %s: %w", dir, err) + } + slog.Debug("librariangen: bazel config loaded", "conf", fmt.Sprintf("%+v", c)) + return c, nil +} + +func findString(content, name string) string { + re := regexp.MustCompile(fmt.Sprintf(`%s\s*=\s*"([^"]+)"`, name)) + if match := re.FindStringSubmatch(content); len(match) > 1 { + return match[1] + } + slog.Debug("librariangen: failed to find string attr in BUILD.bazel", "name", name) + return "" +} + +func findBool(content, name string) (bool, error) { + re := regexp.MustCompile(fmt.Sprintf(`%s\s*=\s*(\w+)`, name)) + if match := re.FindStringSubmatch(content); len(match) > 1 { + if b, err := strconv.ParseBool(match[1]); err == nil { + return b, nil + } + return false, fmt.Errorf("librariangen: failed to parse bool attr in BUILD.bazel: %q, got: %q", name, match[1]) + } + slog.Debug("librariangen: failed to find bool attr in BUILD.bazel", "name", name) + return false, nil +} diff --git a/internal/librariangen/bazel/parser_test.go b/internal/librariangen/bazel/parser_test.go new file mode 100644 index 0000000000..fc141f18c5 --- /dev/null +++ b/internal/librariangen/bazel/parser_test.go @@ -0,0 +1,182 @@ +// 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 bazel + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParse(t *testing.T) { + content := ` +java_grpc_library( + name = "asset_java_grpc", + srcs = [":asset_proto"], + deps = [":asset_java_proto"], +) + +java_gapic_library( + name = "asset_java_gapic", + srcs = [":asset_proto_with_info"], + grpc_service_config = "cloudasset_grpc_service_config.json", + rest_numeric_enums = True, + service_yaml = "cloudasset_v1.yaml", + test_deps = [ + ":asset_java_grpc", + "//google/iam/v1:iam_java_grpc", + ], + transport = "grpc+rest", + deps = [ + ":asset_java_proto", + "//google/api:api_java_proto", + "//google/iam/v1:iam_java_proto", + ], +) +` + tmpDir := t.TempDir() + buildPath := filepath.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := Parse(tmpDir) + if err != nil { + t.Fatalf("Parse() failed: %v", err) + } + + if !got.HasGAPIC() { + t.Error("HasGAPIC() = false; want true") + } + if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want { + t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want) + } + if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want { + t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want) + } + if want := "grpc+rest"; got.Transport() != want { + t.Errorf("Transport() = %q; want %q", got.Transport(), want) + } + if !got.HasRESTNumericEnums() { + t.Error("HasRESTNumericEnums() = false; want true") + } +} + +func TestParse_serviceConfigIsTarget(t *testing.T) { + content := ` +java_grpc_library( + name = "asset_java_grpc", + srcs = [":asset_proto"], + deps = [":asset_java_proto"], +) + +java_gapic_library( + name = "asset_java_gapic", + srcs = [":asset_proto_with_info"], + grpc_service_config = "cloudasset_grpc_service_config.json", + rest_numeric_enums = True, + service_yaml = ":cloudasset_v1.yaml", + test_deps = [ + ":asset_java_grpc", + "//google/iam/v1:iam_java_grpc", + ], + transport = "grpc+rest", + deps = [ + ":asset_java_proto", + "//google/api:api_java_proto", + "//google/iam/v1:iam_java_proto", + ], +) +` + tmpDir := t.TempDir() + buildPath := filepath.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := Parse(tmpDir) + if err != nil { + t.Fatalf("Parse() failed: %v", err) + } + + if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want { + t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want) + } +} + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + { + name: "valid GAPIC", + cfg: &Config{ + hasGAPIC: true, + serviceYAML: "b", + grpcServiceConfig: "c", + transport: "d", + }, + wantErr: false, + }, + { + name: "valid non-GAPIC", + cfg: &Config{}, + wantErr: false, + }, + { + name: "gRPC service config and transport are optional", + cfg: &Config{hasGAPIC: true, serviceYAML: "b"}, + wantErr: false, + }, + { + name: "missing serviceYAML", + cfg: &Config{hasGAPIC: true, grpcServiceConfig: "c", transport: "d"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.cfg.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestParse_noGapic(t *testing.T) { + content := ` +java_grpc_library( + name = "asset_java_grpc", + srcs = [":asset_proto"], + deps = [":asset_java_proto"], +) +` + tmpDir := t.TempDir() + buildPath := filepath.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := Parse(tmpDir) + if err != nil { + t.Fatalf("Parse() failed: %v", err) + } + + if got.HasGAPIC() { + t.Error("HasGAPIC() = true; want false") + } +} \ No newline at end of file From 38fc4c522ea578b585b69d6b643f2f681d6d8f27 Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Wed, 15 Oct 2025 03:52:49 +0000 Subject: [PATCH 2/4] test: improve coverage --- internal/librariangen/bazel/parser_test.go | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/librariangen/bazel/parser_test.go b/internal/librariangen/bazel/parser_test.go index fc141f18c5..103080564f 100644 --- a/internal/librariangen/bazel/parser_test.go +++ b/internal/librariangen/bazel/parser_test.go @@ -179,4 +179,60 @@ java_grpc_library( if got.HasGAPIC() { t.Error("HasGAPIC() = true; want false") } +} + +func TestParse_missingSomeAttrs(t *testing.T) { + content := ` +java_gapic_library( + name = "asset_java_gapic", + service_yaml = "cloudasset_v1.yaml", +) +` + tmpDir := t.TempDir() + buildPath := filepath.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := Parse(tmpDir) + if err != nil { + t.Fatalf("Parse() failed: %v", err) + } + + if got.GRPCServiceConfig() != "" { + t.Errorf("GRPCServiceConfig() = %q; want \"\"", got.GRPCServiceConfig()) + } + if got.Transport() != "" { + t.Errorf("Transport() = %q; want \"\"", got.Transport()) + } + if got.HasRESTNumericEnums() { + t.Error("HasRESTNumericEnums() = true; want false") + } +} + +func TestParse_invalidBoolAttr(t *testing.T) { + content := ` +java_gapic_library( + name = "asset_java_gapic", + rest_numeric_enums = "not-a-bool", +) +` + tmpDir := t.TempDir() + buildPath := filepath.Join(tmpDir, "BUILD.bazel") + if err := os.WriteFile(buildPath, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + _, err := Parse(tmpDir) + if err == nil { + t.Error("Parse() succeeded; want error") + } +} + +func TestParse_noBuildFile(t *testing.T) { + tmpDir := t.TempDir() + _, err := Parse(tmpDir) + if err == nil { + t.Error("Parse() succeeded; want error") + } } \ No newline at end of file From 69b160de9bcc4d40054330d61cee0e4c102e912c Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Thu, 16 Oct 2025 01:35:24 +0000 Subject: [PATCH 3/4] address review feedback --- internal/librariangen/bazel/parser.go | 22 +++++++++--- internal/librariangen/bazel/parser_test.go | 40 ++++++++++++++-------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/internal/librariangen/bazel/parser.go b/internal/librariangen/bazel/parser.go index 321cbed76c..f0b2a0a550 100644 --- a/internal/librariangen/bazel/parser.go +++ b/internal/librariangen/bazel/parser.go @@ -23,6 +23,7 @@ import ( "regexp" "strconv" "strings" + "sync" ) // Config holds configuration extracted from a googleapis BUILD.bazel file. @@ -59,6 +60,7 @@ func (c *Config) Validate() error { return nil } +var javaGapicLibraryRE = regexp.MustCompile(`java_gapic_library\((?s:.)*?\)`) // Parse reads a BUILD.bazel file from the given directory and extracts the // relevant configuration from the java_gapic_library rule. func Parse(dir string) (*Config, error) { @@ -70,8 +72,7 @@ func Parse(dir string) (*Config, error) { } content := string(data) - re := regexp.MustCompile(`java_gapic_library\((?s:.)*?\)`) - gapicLibraryBlock := re.FindString(content) + gapicLibraryBlock := javaGapicLibraryRE.FindString(content) if gapicLibraryBlock != "" { c.hasGAPIC = true c.grpcServiceConfig = findString(gapicLibraryBlock, "grpc_service_config") @@ -84,12 +85,23 @@ func Parse(dir string) (*Config, error) { if err := c.Validate(); err != nil { return nil, fmt.Errorf("librariangen: invalid bazel config in %s: %w", dir, err) } - slog.Debug("librariangen: bazel config loaded", "conf", fmt.Sprintf("%+v", c)) + slog.Debug("librariangen: bazel config loaded", "conf", c) return c, nil } +var reCache = &sync.Map{} + +func getRegexp(key, pattern string) *regexp.Regexp { + val, ok := reCache.Load(key) + if !ok { + val = regexp.MustCompile(pattern) + reCache.Store(key, val) + } + return val.(*regexp.Regexp) +} + func findString(content, name string) string { - re := regexp.MustCompile(fmt.Sprintf(`%s\s*=\s*"([^"]+)"`, name)) + re := getRegexp("findString_"+name, fmt.Sprintf(`%s\s*=\s*"([^"]+)"`, name)) if match := re.FindStringSubmatch(content); len(match) > 1 { return match[1] } @@ -98,7 +110,7 @@ func findString(content, name string) string { } func findBool(content, name string) (bool, error) { - re := regexp.MustCompile(fmt.Sprintf(`%s\s*=\s*(\w+)`, name)) + re := getRegexp("findBool_"+name, fmt.Sprintf(`%s\s*=\s*(\w+)`, name)) if match := re.FindStringSubmatch(content); len(match) > 1 { if b, err := strconv.ParseBool(match[1]); err == nil { return b, nil diff --git a/internal/librariangen/bazel/parser_test.go b/internal/librariangen/bazel/parser_test.go index 103080564f..104e4376de 100644 --- a/internal/librariangen/bazel/parser_test.go +++ b/internal/librariangen/bazel/parser_test.go @@ -57,21 +57,31 @@ java_gapic_library( t.Fatalf("Parse() failed: %v", err) } - if !got.HasGAPIC() { - t.Error("HasGAPIC() = false; want true") - } - if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want { - t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want) - } - if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want { - t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want) - } - if want := "grpc+rest"; got.Transport() != want { - t.Errorf("Transport() = %q; want %q", got.Transport(), want) - } - if !got.HasRESTNumericEnums() { - t.Error("HasRESTNumericEnums() = false; want true") - } + t.Run("HasGAPIC", func(t *testing.T) { + if !got.HasGAPIC() { + t.Error("HasGAPIC() = false; want true") + } + }) + t.Run("ServiceYAML", func(t *testing.T) { + if want := "cloudasset_v1.yaml"; got.ServiceYAML() != want { + t.Errorf("ServiceYAML() = %q; want %q", got.ServiceYAML(), want) + } + }) + t.Run("GRPCServiceConfig", func(t *testing.T) { + if want := "cloudasset_grpc_service_config.json"; got.GRPCServiceConfig() != want { + t.Errorf("GRPCServiceConfig() = %q; want %q", got.GRPCServiceConfig(), want) + } + }) + t.Run("Transport", func(t *testing.T) { + if want := "grpc+rest"; got.Transport() != want { + t.Errorf("Transport() = %q; want %q", got.Transport(), want) + } + }) + t.Run("HasRESTNumericEnums", func(t *testing.T) { + if !got.HasRESTNumericEnums() { + t.Error("HasRESTNumericEnums() = false; want true") + } + }) } func TestParse_serviceConfigIsTarget(t *testing.T) { From f17cdb87bd98375db71e8508d8998302dee690e5 Mon Sep 17 00:00:00 2001 From: Mike Eltsufin Date: Thu, 16 Oct 2025 02:08:48 +0000 Subject: [PATCH 4/4] support single quoted strings --- internal/librariangen/bazel/parser.go | 10 +++++++--- internal/librariangen/bazel/parser_test.go | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/librariangen/bazel/parser.go b/internal/librariangen/bazel/parser.go index f0b2a0a550..a041903e1c 100644 --- a/internal/librariangen/bazel/parser.go +++ b/internal/librariangen/bazel/parser.go @@ -101,9 +101,13 @@ func getRegexp(key, pattern string) *regexp.Regexp { } func findString(content, name string) string { - re := getRegexp("findString_"+name, fmt.Sprintf(`%s\s*=\s*"([^"]+)"`, name)) - if match := re.FindStringSubmatch(content); len(match) > 1 { - return match[1] + re := getRegexp("findString_"+name, fmt.Sprintf(`%s\s*=\s*(?:"([^"]+)"|'([^']+)'){1}`, name)) + match := re.FindStringSubmatch(content) + if len(match) > 2 { + if match[1] != "" { + return match[1] // Double-quoted + } + return match[2] // Single-quoted } slog.Debug("librariangen: failed to find string attr in BUILD.bazel", "name", name) return "" diff --git a/internal/librariangen/bazel/parser_test.go b/internal/librariangen/bazel/parser_test.go index 104e4376de..992665d76d 100644 --- a/internal/librariangen/bazel/parser_test.go +++ b/internal/librariangen/bazel/parser_test.go @@ -38,7 +38,7 @@ java_gapic_library( ":asset_java_grpc", "//google/iam/v1:iam_java_grpc", ], - transport = "grpc+rest", + transport = 'grpc+rest', deps = [ ":asset_java_proto", "//google/api:api_java_proto",