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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 148 additions & 64 deletions tool/cmd/migrate/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@ import (
"slices"
"strings"

"github.com/bazelbuild/buildtools/build"
"github.com/googleapis/librarian/internal/config"
"github.com/googleapis/librarian/internal/librarian"
"github.com/googleapis/librarian/internal/yaml"
)

var (
versionedAPIPath = regexp.MustCompile(`^/(.+/(v\d+\w*))/(.+)-ruby/(.*)$`)
// regexAPIPath matches OwlBot deep-copy-regex source paths for Ruby libraries.
// Capturing groups:
// 1: Base API path (e.g. "google/cloud/automl")
// 2: API version (e.g. "v1") or empty string for unversioned wrapper libraries
// 3: Gem directory name token (e.g. "[^/]+" or "google-cloud-automl-v1")
// 4: Trailing path contents after "-ruby/"
regexAPIPath = regexp.MustCompile(`^/(.+?)(?:/(v\d+\w*))?/(\[\^/\]\+|[^/]+)-ruby/(.*)$`)
// Skip these directories when searching for libraries.
skippedDirs = []string{".github"}
)
Expand All @@ -45,8 +52,14 @@ type owlbotSrc struct {
Source string `yaml:"source"`
}

// VersionedBuild represents build configuration parsed from BUILD.bazel for a Ruby API version.
type VersionedBuild struct {
// WrapperBuild represents build configuration parsed from BUILD.bazel for an unversioned Ruby wrapper library.
type WrapperBuild struct {
Path string
Params *ExtraProtoParams
}

// ExtraProtoParams represents extra protoc parameters parsed from BUILD.bazel for a Ruby API version.
type ExtraProtoParams struct {
EnvPrefix string
ExtraDeps string
GemNamespace string
Expand Down Expand Up @@ -141,63 +154,94 @@ func findRubyLibraries(googleapisPath, repoPath string) ([]*config.Library, erro
lib := &config.Library{
Name: name,
}
api, err := parseAPIFromOwlBot(owlBotPath)
api, isWrapper, err := parseAPIFromOwlBot(owlBotPath)
if err != nil {
return nil, err
}
if api != "" {
lib.APIs = []*config.API{
{
Path: api,
},
}
vb, err := parseVersionedBuild(googleapisPath, api)
if err != nil {
return nil, err
}
if vb != nil {
lib.APIs[0].Ruby = &config.RubyAPI{
RubyCloudOpts: &config.RubyCloudOpts{
EnvPrefix: vb.EnvPrefix,
ExtraDependencies: vb.ExtraDeps,
GemNamespace: vb.GemNamespace,
NamespaceOverride: vb.NamespaceOverride,
PathOverride: vb.PathOverride,
ServiceOverride: vb.ServiceOverride,
WrapperGemOverride: vb.WrapperGemOverride,
YardStrict: vb.YardStrict,
if !isWrapper {
lib.APIs = []*config.API{
{
Path: api,
},
}
vb, err := parseVersionedBuild(googleapisPath, api)
if err != nil {
return nil, err
}
if vb != nil {
lib.APIs[0].Ruby = &config.RubyAPI{
RubyCloudOpts: &config.RubyCloudOpts{
EnvPrefix: vb.EnvPrefix,
ExtraDependencies: vb.ExtraDeps,
GemNamespace: vb.GemNamespace,
NamespaceOverride: vb.NamespaceOverride,
PathOverride: vb.PathOverride,
ServiceOverride: vb.ServiceOverride,
WrapperGemOverride: vb.WrapperGemOverride,
YardStrict: vb.YardStrict,
},
}
}
} else {
wb, err := parseUnversionedBuild(googleapisPath, api)
if err != nil {
return nil, err
}
if wb != nil {
rubyAPI := &config.API{
Path: wb.Path,
Ruby: &config.RubyAPI{
RubyCloudOpts: &config.RubyCloudOpts{
EnvPrefix: wb.Params.EnvPrefix,
ExtraDependencies: wb.Params.ExtraDeps,
GemNamespace: wb.Params.GemNamespace,
NamespaceOverride: wb.Params.NamespaceOverride,
PathOverride: wb.Params.PathOverride,
ServiceOverride: wb.Params.ServiceOverride,
WrapperGemOverride: wb.Params.WrapperGemOverride,
YardStrict: wb.Params.YardStrict,
},
},
}
lib.APIs = append(lib.APIs, rubyAPI)
}
}
libraries = append(libraries, lib)
}
libraries = append(libraries, lib)
}
parseWrapperOf(libraries)
return libraries, nil
}

func parseAPIFromOwlBot(owlBotPath string) (string, error) {
// parseAPIFromOwlBot parses API details from OwlBot config and determines if the library is a wrapper.
// It returns the API path, whether the library is a wrapper, and an error if parsing fails.
func parseAPIFromOwlBot(owlBotPath string) (string, bool, error) {
data, err := os.ReadFile(owlBotPath)
if err != nil {
return "", fmt.Errorf("reading OwlBot config %s: %w", owlBotPath, err)
return "", false, fmt.Errorf("reading OwlBot config %s: %w", owlBotPath, err)
}
owlbot, err := yaml.Unmarshal[owlbotYaml](data)
if err != nil {
return "", fmt.Errorf("parsing OwlBot config %s: %w", owlBotPath, err)
return "", false, fmt.Errorf("parsing OwlBot config %s: %w", owlBotPath, err)
}
// Skip .github/.Owlbot.yaml.
if len(owlbot.DeepCopyRegex) == 0 {
return "", nil
return "", false, nil
}
// We only need the first entry since wrapper library will
// have different parsing logic.
src := owlbot.DeepCopyRegex[0].Source
matches := versionedAPIPath.FindStringSubmatch(src)
matches := regexAPIPath.FindStringSubmatch(src)
if len(matches) != 5 {
// A wrapper library doesn't have versioned API path.
return "", nil
return "", false, nil
}
return matches[1], nil
basePath := matches[1]
version := matches[2]
if version == "" {
// Unversioned wrapper library
return basePath, true, nil
}
// Versioned library
return basePath + "/" + version, false, nil
}

// parseWrapperOf sets the WrapperOf field for wrapper libraries.
Expand All @@ -206,10 +250,6 @@ func parseWrapperOf(libraries []*config.Library) {
return strings.Compare(a.Name, b.Name)
})
for i, lib := range libraries {
if len(lib.APIs) != 0 {
// Skip non-wrapper libraries.
continue
}
var wrapperOf []string
prefix := lib.Name + "-"
// Since libraries are sorted by name, the wrapped libraries
Expand Down Expand Up @@ -238,38 +278,42 @@ func parseWrapperOf(libraries []*config.Library) {
}
}

func parseVersionedBuild(googleapisDir, apiPath string) (*VersionedBuild, error) {
func parseVersionedBuild(googleapisDir, apiPath string) (*ExtraProtoParams, error) {
file, err := parseBazel(googleapisDir, apiPath)
if err != nil {
return nil, err
}
if file == nil {
return nil, nil
}
vb := &VersionedBuild{}
if rules := file.Rules("ruby_cloud_gapic_library"); len(rules) > 0 {
rule := rules[0]
if attr := rule.Attr("extra_protoc_parameters"); attr != nil {
for _, dep := range extractStrings(attr) {
switch {
case strings.HasPrefix(dep, "ruby-cloud-env-prefix="):
vb.EnvPrefix, _ = strings.CutPrefix(dep, "ruby-cloud-env-prefix=")
case strings.HasPrefix(dep, "ruby-cloud-extra-dependencies="):
vb.ExtraDeps, _ = strings.CutPrefix(dep, "ruby-cloud-extra-dependencies=")
case strings.HasPrefix(dep, "ruby-cloud-gem-namespace="):
vb.GemNamespace, _ = strings.CutPrefix(dep, "ruby-cloud-gem-namespace=")
case strings.HasPrefix(dep, "ruby-cloud-namespace-override="):
vb.NamespaceOverride, _ = strings.CutPrefix(dep, "ruby-cloud-namespace-override=")
case strings.HasPrefix(dep, "ruby-cloud-path-override="):
vb.PathOverride, _ = strings.CutPrefix(dep, "ruby-cloud-path-override=")
case strings.HasPrefix(dep, "ruby-cloud-service-override="):
vb.ServiceOverride, _ = strings.CutPrefix(dep, "ruby-cloud-service-override=")
case strings.HasPrefix(dep, "ruby-cloud-wrapper-gem-override="):
vb.WrapperGemOverride, _ = strings.CutPrefix(dep, "ruby-cloud-wrapper-gem-override=")
case strings.HasPrefix(dep, "ruby-cloud-yard-strict="):
vb.YardStrict, _ = strings.CutPrefix(dep, "ruby-cloud-yard-strict=")
}
}
return parseExtraProtoParams(file)
}

func parseExtraProtoParams(file *build.File) (*ExtraProtoParams, error) {
vb := &ExtraProtoParams{}
rules := file.Rules("ruby_cloud_gapic_library")
if len(rules) == 0 || rules[0].Attr("extra_protoc_parameters") == nil {
return vb, nil
}
for _, dep := range extractStrings(rules[0].Attr("extra_protoc_parameters")) {
switch {
case strings.HasPrefix(dep, "ruby-cloud-env-prefix="):
vb.EnvPrefix, _ = strings.CutPrefix(dep, "ruby-cloud-env-prefix=")
case strings.HasPrefix(dep, "ruby-cloud-extra-dependencies="):
vb.ExtraDeps, _ = strings.CutPrefix(dep, "ruby-cloud-extra-dependencies=")
case strings.HasPrefix(dep, "ruby-cloud-gem-namespace="):
vb.GemNamespace, _ = strings.CutPrefix(dep, "ruby-cloud-gem-namespace=")
case strings.HasPrefix(dep, "ruby-cloud-namespace-override="):
vb.NamespaceOverride, _ = strings.CutPrefix(dep, "ruby-cloud-namespace-override=")
case strings.HasPrefix(dep, "ruby-cloud-path-override="):
vb.PathOverride, _ = strings.CutPrefix(dep, "ruby-cloud-path-override=")
case strings.HasPrefix(dep, "ruby-cloud-service-override="):
vb.ServiceOverride, _ = strings.CutPrefix(dep, "ruby-cloud-service-override=")
case strings.HasPrefix(dep, "ruby-cloud-wrapper-gem-override="):
vb.WrapperGemOverride, _ = strings.CutPrefix(dep, "ruby-cloud-wrapper-gem-override=")
case strings.HasPrefix(dep, "ruby-cloud-yard-strict="):
vb.YardStrict, _ = strings.CutPrefix(dep, "ruby-cloud-yard-strict=")

}
}
return vb, nil
Expand Down Expand Up @@ -327,3 +371,43 @@ func readExistingConfig(repoPath string) (*config.Config, error) {
}
return cfg, nil
}

func parseUnversionedBuild(googleapisDir, apiPath string) (*WrapperBuild, error) {
file, err := parseBazel(googleapisDir, apiPath)
if err != nil {
return nil, err
}
if file == nil {
return nil, nil
}
api := parseAPIFromWrapperBuild(file)
if api == "" {
return nil, nil
}
params, err := parseExtraProtoParams(file)
if err != nil {
return nil, err
}
return &WrapperBuild{
Path: api,
Params: params,
}, nil
}

func parseAPIFromWrapperBuild(file *build.File) string {
rules := file.Rules("ruby_cloud_gapic_library")
if len(rules) == 0 || rules[0].Attr("srcs") == nil {
return ""
}
srcs := extractStrings(rules[0].Attr("srcs"))
if len(srcs) == 0 {
return ""
}
res := srcs[0]
parts := strings.SplitN(res, ":", 2)
if len(parts) > 0 {
res = parts[0]
}
res, _ = strings.CutPrefix(res, "//")
return res
}
Loading
Loading