Skip to content

Commit b063e4b

Browse files
authored
feat(internal/serviceconfig): add Find to add default paths (#3096)
FindServiceConfig locates the service config file for an API path by scanning for YAML files containing `type: google.api.Service` in the first 5 lines, while skipping `_gapic.yaml` files. The generate command now automatically populates the ServiceConfig field for APIs that don't have one explicitly configured. This reduces manual configuration when adding new libraries, when we can determine the service config path from the API path.
1 parent de21d85 commit b063e4b

File tree

6 files changed

+170
-10
lines changed

6 files changed

+170
-10
lines changed

internal/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type Source struct {
6060

6161
// Dir is a local directory path to use instead of fetching.
6262
// If set, Commit and SHA256 are ignored.
63-
Dir string `yaml:"-"`
63+
Dir string `yaml:"dir,omitempty"`
6464
}
6565

6666
// Default contains default settings for all libraries.

internal/librarian/generate.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ import (
2020
"fmt"
2121

2222
"github.com/googleapis/librarian/internal/config"
23+
"github.com/googleapis/librarian/internal/fetch"
2324
"github.com/googleapis/librarian/internal/librarian/internal/rust"
25+
"github.com/googleapis/librarian/internal/serviceconfig"
2426
"github.com/googleapis/librarian/internal/yaml"
2527
"github.com/urfave/cli/v3"
2628
)
2729

30+
const googleapisRepo = "github.com/googleapis/googleapis"
31+
2832
var (
2933
errMissingLibraryOrAllFlag = errors.New("must specify library name or use --all flag")
3034
errBothLibraryAndAllFlag = errors.New("cannot specify both library name and --all flag")
@@ -71,21 +75,30 @@ func runGenerate(ctx context.Context, all bool, libraryName string) error {
7175
}
7276

7377
func generateAll(ctx context.Context, cfg *config.Config) error {
74-
var errs []error
7578
for _, lib := range cfg.Libraries {
76-
if err := generate(ctx, cfg.Language, lib, cfg.Sources); err != nil {
77-
errs = append(errs, err)
79+
if err := generateLibrary(ctx, cfg, lib.Name); err != nil {
80+
return err
7881
}
7982
}
80-
if len(errs) > 0 {
81-
return errors.Join(errs...)
82-
}
8383
return nil
8484
}
8585

8686
func generateLibrary(ctx context.Context, cfg *config.Config, libraryName string) error {
87+
googleapisDir, err := fetchGoogleapisDir(ctx, cfg.Sources)
88+
if err != nil {
89+
return err
90+
}
8791
for _, lib := range cfg.Libraries {
8892
if lib.Name == libraryName {
93+
for _, api := range lib.Channels {
94+
if api.ServiceConfig == "" {
95+
serviceConfig, err := serviceconfig.Find(googleapisDir, api.Path)
96+
if err != nil {
97+
return err
98+
}
99+
api.ServiceConfig = serviceConfig
100+
}
101+
}
89102
return generate(ctx, cfg.Language, lib, cfg.Sources)
90103
}
91104
}
@@ -102,11 +115,20 @@ func generate(ctx context.Context, language string, library *config.Library, sou
102115
default:
103116
err = fmt.Errorf("generate not implemented for %q", language)
104117
}
105-
106118
if err != nil {
107119
fmt.Printf("✗ Error generating %s: %v\n", library.Name, err)
108120
return err
109121
}
110122
fmt.Printf("✓ Successfully generated %s\n", library.Name)
111123
return nil
112124
}
125+
126+
func fetchGoogleapisDir(ctx context.Context, sources *config.Sources) (string, error) {
127+
if sources == nil || sources.Googleapis == nil {
128+
return "", errors.New("googleapis source is required")
129+
}
130+
if sources.Googleapis.Dir != "" {
131+
return sources.Googleapis.Dir, nil
132+
}
133+
return fetch.RepoDir(ctx, googleapisRepo, sources.Googleapis.Commit, sources.Googleapis.SHA256)
134+
}

internal/librarian/generate_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,31 @@ func TestGenerateCommand(t *testing.T) {
3131
lib2 = "library-two"
3232
lib2Output = "output2"
3333
)
34+
wd, err := os.Getwd()
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
googleapisDir := filepath.Join(wd, "testdata", "googleapis")
39+
3440
tempDir := t.TempDir()
3541
t.Chdir(tempDir)
3642
configPath := filepath.Join(tempDir, librarianConfigPath)
43+
3744
configContent := fmt.Sprintf(`language: testhelper
3845
sources:
3946
googleapis:
40-
commit: abc123
47+
dir: %s
4148
libraries:
4249
- name: %s
4350
output: %s
51+
apis:
52+
- path: google/cloud/speech/v1
53+
- path: google/cloud/speech/v1p1beta1
54+
- path: google/cloud/speech/v2
55+
- path: grafeas/v1
4456
- name: %s
4557
output: %s
46-
`, lib1, lib1Output, lib2, lib2Output)
58+
`, googleapisDir, lib1, lib1Output, lib2, lib2Output)
4759
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
4860
t.Fatal(err)
4961
}

internal/serviceconfig/serviceconfig.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
package serviceconfig
1717

1818
import (
19+
"bufio"
1920
"encoding/json"
2021
"fmt"
2122
"os"
23+
"path/filepath"
24+
"strings"
2225

2326
"github.com/googleapis/librarian/internal/yaml"
2427
"google.golang.org/genproto/googleapis/api/serviceconfig"
@@ -67,3 +70,57 @@ func Read(serviceConfigPath string) (*Service, error) {
6770
}
6871
return cfg, nil
6972
}
73+
74+
// Find finds the service config file for a channel path. It looks for YAML
75+
// files containing "type: google.api.Service", skipping any files ending in
76+
// _gapic.yaml.
77+
//
78+
// The apiPath should be relative to googleapisDir (e.g.,
79+
// "google/cloud/secretmanager/v1"). Returns the service config path relative
80+
// to googleapisDir, or empty string if not found.
81+
func Find(googleapisDir, apiPath string) (string, error) {
82+
dir := filepath.Join(googleapisDir, apiPath)
83+
entries, err := os.ReadDir(dir)
84+
if err != nil {
85+
return "", err
86+
}
87+
for _, entry := range entries {
88+
if entry.IsDir() {
89+
continue
90+
}
91+
name := entry.Name()
92+
if !strings.HasSuffix(name, ".yaml") {
93+
continue
94+
}
95+
if strings.HasSuffix(name, "_gapic.yaml") {
96+
continue
97+
}
98+
99+
path := filepath.Join(dir, name)
100+
isServiceConfig, err := isServiceConfigFile(path)
101+
if err != nil {
102+
return "", err
103+
}
104+
if isServiceConfig {
105+
return filepath.Join(apiPath, name), nil
106+
}
107+
}
108+
return "", nil
109+
}
110+
111+
// isServiceConfigFile checks if the file contains "type: google.api.Service".
112+
func isServiceConfigFile(path string) (bool, error) {
113+
f, err := os.Open(path)
114+
if err != nil {
115+
return false, err
116+
}
117+
defer f.Close()
118+
119+
scanner := bufio.NewScanner(f)
120+
for i := 0; i < 20 && scanner.Scan(); i++ {
121+
if strings.TrimSpace(scanner.Text()) == "type: google.api.Service" {
122+
return true, nil
123+
}
124+
}
125+
return false, scanner.Err()
126+
}

internal/serviceconfig/serviceconfig_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,14 @@ func TestNoGenprotoServiceConfigImports(t *testing.T) {
7777
len(violations), genprotoImport, strings.Join(violations, "\n "))
7878
}
7979
}
80+
81+
func TestFind(t *testing.T) {
82+
got, err := Find("testdata/googleapis", "google/cloud/speech/v1")
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
want := "google/cloud/speech/v1/speech_v1.yaml"
87+
if got != want {
88+
t.Errorf("got %q, want %q", got, want)
89+
}
90+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
# https://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+
type: google.api.Service
15+
config_version: 3
16+
name: speech.googleapis.com
17+
title: Cloud Speech-to-Text API
18+
apis:
19+
- name: google.cloud.speech.v1.Adaptation
20+
- name: google.cloud.speech.v1.Speech
21+
- name: google.longrunning.Operations
22+
types:
23+
- name: google.cloud.speech.v1.LongRunningRecognizeMetadata
24+
- name: google.cloud.speech.v1.LongRunningRecognizeResponse
25+
documentation:
26+
summary: Converts audio to text by applying powerful neural network models.
27+
overview: |-
28+
# Introduction
29+
30+
Google Cloud Speech API provides speech recognition as a service.
31+
backend:
32+
rules:
33+
- selector: 'google.cloud.speech.v1.Adaptation.*'
34+
deadline: 355.0
35+
- selector: 'google.cloud.speech.v1.Speech.*'
36+
deadline: 355.0
37+
- selector: 'google.longrunning.Operations.*'
38+
deadline: 355.0
39+
http:
40+
rules:
41+
- selector: google.longrunning.Operations.GetOperation
42+
get: '/v1/operations/{name=**}'
43+
- selector: google.longrunning.Operations.ListOperations
44+
get: /v1/operations
45+
authentication:
46+
rules:
47+
- selector: 'google.cloud.speech.v1.Adaptation.*'
48+
oauth:
49+
canonical_scopes: |-
50+
https://www.googleapis.com/auth/cloud-platform
51+
- selector: 'google.cloud.speech.v1.Speech.*'
52+
oauth:
53+
canonical_scopes: |-
54+
https://www.googleapis.com/auth/cloud-platform
55+
- selector: 'google.longrunning.Operations.*'
56+
oauth:
57+
canonical_scopes: |-
58+
https://www.googleapis.com/auth/cloud-platform

0 commit comments

Comments
 (0)