Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions internal/librariangen/bazel/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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"
"sync"
)

// 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
}

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) {
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)

gapicLibraryBlock := javaGapicLibraryRE.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", 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 := getRegexp("findString_"+name, 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 := 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
}
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
}
248 changes: 248 additions & 0 deletions internal/librariangen/bazel/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// 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)
}

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) {
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")
}
}

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")
}
}
Loading