Skip to content

Commit 10d73ea

Browse files
authored
feat(internal/librarian): add allowlist (#3177)
Create an allowlist of libraries that we expected to be generated across all client libraries, starting with Rust. Use this list to determine libraries that should be generated using default values when running `librarian generate --all`. Fixes #3076
1 parent dfe19e6 commit 10d73ea

File tree

5 files changed

+447
-56
lines changed

5 files changed

+447
-56
lines changed

internal/librarian/generate.go

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"io/fs"
2222
"os"
2323
"path/filepath"
24-
"strings"
2524

2625
"github.com/googleapis/librarian/internal/config"
2726
"github.com/googleapis/librarian/internal/fetch"
@@ -79,6 +78,16 @@ func runGenerate(ctx context.Context, all bool, libraryName string) error {
7978
}
8079

8180
func generateAll(ctx context.Context, cfg *config.Config) error {
81+
googleapisDir, err := fetchGoogleapisDir(ctx, cfg.Sources)
82+
if err != nil {
83+
return err
84+
}
85+
86+
libraries, err := deriveDefaultLibraries(cfg, googleapisDir)
87+
if err != nil {
88+
return err
89+
}
90+
cfg.Libraries = append(cfg.Libraries, libraries...)
8291
for _, lib := range cfg.Libraries {
8392
if err := generateLibrary(ctx, cfg, lib.Name); err != nil {
8493
return err
@@ -87,6 +96,77 @@ func generateAll(ctx context.Context, cfg *config.Config) error {
8796
return nil
8897
}
8998

99+
// deriveDefaultLibraries finds libraries for allowed channels that are not
100+
// explicitly configured in librarian.yaml.
101+
//
102+
// For each allowed channel without configuration, it derives default values
103+
// for the library name and output path. If the output directory exists, the
104+
// library is added for generation. Channels whose output directories do not
105+
// exist in the librarian.yaml but should be generated are returned.
106+
func deriveDefaultLibraries(cfg *config.Config, googleapisDir string) ([]*config.Library, error) {
107+
if cfg.Default == nil {
108+
return nil, nil
109+
}
110+
111+
configured := make(map[string]bool)
112+
for _, lib := range cfg.Libraries {
113+
for _, ch := range lib.Channels {
114+
configured[ch.Path] = true
115+
}
116+
}
117+
118+
var derived []*config.Library
119+
for channel := range serviceconfig.Allowlist {
120+
if configured[channel] {
121+
continue
122+
}
123+
name := defaultLibraryName(cfg.Language, channel)
124+
output := defaultOutput(cfg.Language, channel, cfg.Default.Output)
125+
if !dirExists(output) {
126+
continue
127+
}
128+
sc, err := serviceconfig.Find(googleapisDir, channel)
129+
if err != nil {
130+
return nil, err
131+
}
132+
derived = append(derived, &config.Library{
133+
Name: name,
134+
Output: output,
135+
Channels: []*config.Channel{{
136+
Path: channel,
137+
ServiceConfig: sc,
138+
}},
139+
})
140+
}
141+
return derived, nil
142+
}
143+
144+
func defaultLibraryName(language, channel string) string {
145+
switch language {
146+
case "rust":
147+
return rust.DefaultLibraryName(channel)
148+
default:
149+
return channel
150+
}
151+
}
152+
153+
func defaultOutput(language, channel, defaultOut string) string {
154+
switch language {
155+
case "rust":
156+
return rust.DefaultOutput(channel, defaultOut)
157+
default:
158+
return defaultOut
159+
}
160+
}
161+
162+
func dirExists(path string) bool {
163+
info, err := os.Stat(path)
164+
if err != nil {
165+
return false
166+
}
167+
return info.IsDir()
168+
}
169+
90170
func generateLibrary(ctx context.Context, cfg *config.Config, libraryName string) error {
91171
googleapisDir, err := fetchGoogleapisDir(ctx, cfg.Sources)
92172
if err != nil {
@@ -98,7 +178,10 @@ func generateLibrary(ctx context.Context, cfg *config.Config, libraryName string
98178
fmt.Printf("⊘ Skipping %s (skip_generate is set)\n", lib.Name)
99179
return nil
100180
}
101-
lib = prepareLibrary(cfg.Language, lib, cfg.Default)
181+
lib, err := prepareLibrary(cfg.Language, lib, cfg.Default)
182+
if err != nil {
183+
return err
184+
}
102185
for _, api := range lib.Channels {
103186
if api.ServiceConfig == "" {
104187
serviceConfig, err := serviceconfig.Find(googleapisDir, api.Path)
@@ -117,26 +200,14 @@ func generateLibrary(ctx context.Context, cfg *config.Config, libraryName string
117200
// prepareLibrary applies language-specific derivations and fills defaults.
118201
// For Rust libraries without an explicit output path, it derives the output
119202
// from the first channel path before applying defaults.
120-
func prepareLibrary(language string, lib *config.Library, defaults *config.Default) *config.Library {
121-
// TODO(https://github.com/googleapis/librarian/issues/2966):
122-
// refactor so that the switch statement logic is in one place
123-
if language == "rust" && lib.Output == "" && len(lib.Channels) > 0 {
124-
lib.Output = deriveDefaultRustOutput(lib.Channels[0].Path, defaults.Output)
203+
func prepareLibrary(language string, lib *config.Library, defaults *config.Default) (*config.Library, error) {
204+
if lib.Output == "" {
205+
if len(lib.Channels) == 0 {
206+
return nil, fmt.Errorf("library %q has no channels, cannot determine default output", lib.Name)
207+
}
208+
lib.Output = defaultOutput(language, lib.Channels[0].Path, defaults.Output)
125209
}
126-
return fillDefaults(lib, defaults)
127-
}
128-
129-
// deriveDefaultRustOutput returns the output path for a Rust library. If the
130-
// library has an explicit output path that differs from the default, it returns
131-
// that path. Otherwise, it derives the output from the first channel path by
132-
// stripping the "google/" prefix and joining with the default output. For
133-
// example, the default output for google/cloud/secretmanager/v1 is
134-
// src/generated/cloud/secretmanager/v1.
135-
//
136-
// TODO(https://github.com/googleapis/librarian/issues/2966): refactor and move
137-
// to internal/rust package.
138-
func deriveDefaultRustOutput(channel, defaultOutput string) string {
139-
return filepath.Join(defaultOutput, strings.TrimPrefix(channel, "google/"))
210+
return fillDefaults(lib, defaults), nil
140211
}
141212

142213
func generate(ctx context.Context, language string, library *config.Library, sources *config.Sources) error {
@@ -147,7 +218,7 @@ func generate(ctx context.Context, language string, library *config.Library, sou
147218
case "rust":
148219
keep := append(library.Keep, "Cargo.toml")
149220
if err := cleanOutput(library.Output, keep); err != nil {
150-
return err
221+
return fmt.Errorf("library %s: %w", library.Name, err)
151222
}
152223
err = rust.Generate(ctx, library, sources)
153224
default:

internal/librarian/generate_test.go

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ func TestPrepareLibrary(t *testing.T) {
223223
output string
224224
channels []*config.Channel
225225
want string
226+
wantErr bool
226227
}{
227228
{
228229
name: "empty output derives path from channel",
@@ -244,10 +245,10 @@ func TestPrepareLibrary(t *testing.T) {
244245
want: "src/generated",
245246
},
246247
{
247-
name: "rust with no channels uses default",
248+
name: "rust with no channels returns error",
248249
language: "rust",
249250
channels: nil,
250-
want: "src/generated",
251+
wantErr: true,
251252
},
252253
} {
253254
t.Run(test.name, func(t *testing.T) {
@@ -259,45 +260,23 @@ func TestPrepareLibrary(t *testing.T) {
259260
defaults := &config.Default{
260261
Output: "src/generated",
261262
}
262-
got := prepareLibrary(test.language, lib, defaults)
263+
got, err := prepareLibrary(test.language, lib, defaults)
264+
if test.wantErr {
265+
if err == nil {
266+
t.Fatal("expected error, got nil")
267+
}
268+
return
269+
}
270+
if err != nil {
271+
t.Fatal(err)
272+
}
263273
if got.Output != test.want {
264274
t.Errorf("got output %q, want %q", got.Output, test.want)
265275
}
266276
})
267277
}
268278
}
269279

270-
func TestDeriveDefaultRustOutput(t *testing.T) {
271-
for _, test := range []struct {
272-
name string
273-
channel string
274-
want string
275-
}{
276-
{
277-
name: "standard google cloud path",
278-
channel: "google/cloud/secretmanager/v1",
279-
want: "src/generated/cloud/secretmanager/v1",
280-
},
281-
{
282-
name: "nested api path",
283-
channel: "google/cloud/aiplatform/v1beta1",
284-
want: "src/generated/cloud/aiplatform/v1beta1",
285-
},
286-
{
287-
name: "non-cloud path",
288-
channel: "google/iam/v1",
289-
want: "src/generated/iam/v1",
290-
},
291-
} {
292-
t.Run(test.name, func(t *testing.T) {
293-
got := deriveDefaultRustOutput(test.channel, "src/generated")
294-
if got != test.want {
295-
t.Errorf("got %q, want %q", got, test.want)
296-
}
297-
})
298-
}
299-
}
300-
301280
func TestCleanOutput(t *testing.T) {
302281
for _, test := range []struct {
303282
name string
@@ -384,3 +363,67 @@ func TestCleanOutput(t *testing.T) {
384363
})
385364
}
386365
}
366+
367+
func TestDeriveDefaultLibrariesSkipsConfigured(t *testing.T) {
368+
cfg := &config.Config{
369+
Language: "rust",
370+
Default: &config.Default{Output: t.TempDir()},
371+
Libraries: []*config.Library{{
372+
Name: "secretmanager",
373+
Channels: []*config.Channel{{Path: "google/cloud/secretmanager/v1"}},
374+
}},
375+
}
376+
derived, err := deriveDefaultLibraries(cfg, t.TempDir())
377+
if err != nil {
378+
t.Fatal(err)
379+
}
380+
if len(derived) != 0 {
381+
t.Errorf("got %d derived libraries, want 0", len(derived))
382+
}
383+
}
384+
385+
func TestDeriveDefaultLibrariesWithOutputDir(t *testing.T) {
386+
outputDir := t.TempDir()
387+
googleapisDir := t.TempDir()
388+
389+
writeServiceConfig(t, googleapisDir, "google/cloud/speech/v2", "speech_v2.yaml")
390+
if err := os.MkdirAll(filepath.Join(outputDir, "cloud/speech/v2"), 0755); err != nil {
391+
t.Fatal(err)
392+
}
393+
394+
cfg := &config.Config{
395+
Language: "rust",
396+
Default: &config.Default{Output: outputDir},
397+
}
398+
derived, err := deriveDefaultLibraries(cfg, googleapisDir)
399+
if err != nil {
400+
t.Fatal(err)
401+
}
402+
if len(derived) != 1 {
403+
t.Fatalf("got %d derived libraries, want 1", len(derived))
404+
}
405+
406+
want := &config.Library{
407+
Name: "google-cloud-speech-v2",
408+
Output: filepath.Join(outputDir, "cloud/speech/v2"),
409+
Channels: []*config.Channel{{
410+
Path: "google/cloud/speech/v2",
411+
ServiceConfig: "google/cloud/speech/v2/speech_v2.yaml",
412+
}},
413+
}
414+
if diff := cmp.Diff(want, derived[0]); diff != "" {
415+
t.Errorf("mismatch (-want +got):\n%s", diff)
416+
}
417+
}
418+
419+
func writeServiceConfig(t *testing.T, googleapisDir, channel, filename string) {
420+
t.Helper()
421+
dir := filepath.Join(googleapisDir, channel)
422+
if err := os.MkdirAll(dir, 0755); err != nil {
423+
t.Fatal(err)
424+
}
425+
content := "type: google.api.Service\nname: test.googleapis.com\n"
426+
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644); err != nil {
427+
t.Fatal(err)
428+
}
429+
}

internal/librarian/internal/rust/generate.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"path/filepath"
21+
"strings"
2122

2223
"github.com/googleapis/librarian/internal/command"
2324
"github.com/googleapis/librarian/internal/config"
@@ -70,3 +71,16 @@ func sourceDir(ctx context.Context, source *config.Source, repo string) (string,
7071
}
7172
return fetch.RepoDir(ctx, repo, source.Commit, source.SHA256)
7273
}
74+
75+
// DefaultLibraryName derives a library name from a channel path.
76+
// For example: google/cloud/secretmanager/v1 -> google-cloud-secretmanager-v1.
77+
func DefaultLibraryName(channel string) string {
78+
return strings.ReplaceAll(channel, "/", "-")
79+
}
80+
81+
// DefaultOutput derives an output path from a channel path and default output.
82+
// For example: google/cloud/secretmanager/v1 with default src/generated/
83+
// returns src/generated/cloud/secretmanager/v1.
84+
func DefaultOutput(channel, defaultOutput string) string {
85+
return filepath.Join(defaultOutput, strings.TrimPrefix(channel, "google/"))
86+
}

0 commit comments

Comments
 (0)