Skip to content

Commit 9c6f9c6

Browse files
authored
feat(internal/librarian): remove name argument from add command (#3795)
Change the add command interface from: `librarian add <name> [apis...]` to `librarian add <apis...>`. The library name is now derived from the first API path using language-specific logic: Update tidy to only remove derivable API paths when there is exactly one API. When there are multiple APIs, all paths are preserved. Fixes #3778
1 parent 4ceccb4 commit 9c6f9c6

File tree

8 files changed

+126
-80
lines changed

8 files changed

+126
-80
lines changed

internal/librarian/add.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,58 +21,71 @@ import (
2121
"slices"
2222
"sort"
2323
"strconv"
24+
"strings"
2425
"time"
2526

2627
"github.com/googleapis/librarian/internal/config"
28+
"github.com/googleapis/librarian/internal/librarian/python"
29+
"github.com/googleapis/librarian/internal/librarian/rust"
2730
"github.com/urfave/cli/v3"
2831
)
2932

3033
var (
3134
errLibraryAlreadyExists = errors.New("library already exists in config")
32-
errMissingLibraryName = errors.New("must provide library name")
35+
errMissingAPI = errors.New("must provide at least one API")
3336
errConfigNotFound = errors.New("librarian.yaml not found")
3437
)
3538

3639
func addCommand() *cli.Command {
3740
return &cli.Command{
3841
Name: "add",
3942
Usage: "add a new client library to librarian.yaml",
40-
UsageText: "librarian add <library> [apis...] [flags]",
43+
UsageText: "librarian add <apis...> [flags]",
4144
Action: func(ctx context.Context, c *cli.Command) error {
42-
args := c.Args()
43-
name := args.First()
44-
if name == "" {
45-
return errMissingLibraryName
46-
}
47-
var apis []string
48-
if len(args.Slice()) > 1 {
49-
apis = args.Slice()[1:]
45+
apis := c.Args().Slice()
46+
if len(apis) == 0 {
47+
return errMissingAPI
5048
}
5149
cfg, err := loadConfig()
5250
if err != nil {
5351
return err
5452
}
55-
return runAdd(ctx, cfg, name, apis...)
53+
return runAdd(ctx, cfg, apis...)
5654
},
5755
}
5856
}
5957

60-
func runAdd(ctx context.Context, cfg *config.Config, name string, api ...string) error {
61-
// check for existing libraries, if it exists return an error
58+
func runAdd(ctx context.Context, cfg *config.Config, apis ...string) error {
59+
name := deriveLibraryName(cfg.Language, apis[0])
6260
exists := slices.ContainsFunc(cfg.Libraries, func(lib *config.Library) bool {
6361
return lib.Name == name
6462
})
6563
if exists {
6664
return fmt.Errorf("%w: %s", errLibraryAlreadyExists, name)
6765
}
6866

69-
cfg = addLibraryToLibrarianConfig(cfg, name, api...)
67+
cfg = addLibraryToLibrarianConfig(cfg, name, apis...)
7068
if err := RunTidyOnConfig(ctx, cfg); err != nil {
7169
return err
7270
}
7371
return nil
7472
}
7573

74+
// deriveLibraryName derives a library name from an API path.
75+
// The derivation is language-specific.
76+
func deriveLibraryName(language, api string) string {
77+
switch language {
78+
case languageFake:
79+
return fakeDefaultLibraryName(api)
80+
case languagePython:
81+
return python.DefaultLibraryName(api)
82+
case languageRust:
83+
return rust.DefaultLibraryName(api)
84+
default:
85+
return strings.ReplaceAll(api, "/", "-")
86+
}
87+
}
88+
7689
func addLibraryToLibrarianConfig(cfg *config.Config, name string, api ...string) *config.Config {
7790
lib := &config.Library{
7891
Name: name,

internal/librarian/add_test.go

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,38 @@ func TestAddLibrary(t *testing.T) {
3232
copyrightYear := strconv.Itoa(time.Now().Year())
3333
for _, test := range []struct {
3434
name string
35-
libName string
35+
apis []string
3636
initialLibraries []*config.Library
3737
wantFinalLibraries []*config.Library
3838
wantGeneratedOutputDir string
3939
wantError error
4040
}{
4141
{
4242
name: "create new library",
43-
libName: "google-cloud-secretmanager",
43+
apis: []string{"google/cloud/secretmanager/v1"},
4444
initialLibraries: []*config.Library{},
4545
wantGeneratedOutputDir: "newlib-output",
4646
wantFinalLibraries: []*config.Library{
4747
{
48-
Name: "google-cloud-secretmanager",
48+
Name: "google-cloud-secretmanager-v1",
4949
CopyrightYear: copyrightYear,
5050
},
5151
},
5252
},
5353
{
54-
name: "fail create existing library",
55-
libName: "google-cloud-secretmanager",
54+
name: "fail create existing library",
55+
apis: []string{"google/cloud/secretmanager/v1"},
5656
initialLibraries: []*config.Library{
5757
{
58-
Name: "google-cloud-secretmanager",
58+
Name: "google-cloud-secretmanager-v1",
5959
},
6060
},
6161
wantGeneratedOutputDir: "existing-output",
6262
wantError: errLibraryAlreadyExists,
6363
},
6464
{
65-
name: "create new library and tidy existing",
66-
libName: "google-cloud-secretmanager",
65+
name: "create new library and tidy existing",
66+
apis: []string{"google/cloud/orgpolicy/v1"},
6767
initialLibraries: []*config.Library{
6868
{
6969
Name: "existinglib",
@@ -81,7 +81,7 @@ func TestAddLibrary(t *testing.T) {
8181
},
8282
},
8383
{
84-
Name: "google-cloud-secretmanager",
84+
Name: "google-cloud-orgpolicy-v1",
8585
CopyrightYear: copyrightYear,
8686
},
8787
},
@@ -102,7 +102,7 @@ func TestAddLibrary(t *testing.T) {
102102
if err := yaml.Write(librarianConfigPath, cfg); err != nil {
103103
t.Fatal(err)
104104
}
105-
err = runAdd(t.Context(), cfg, test.libName)
105+
err = runAdd(t.Context(), cfg, test.apis...)
106106
if test.wantError != nil {
107107
if !errors.Is(err, test.wantError) {
108108
t.Errorf("expected error %v, got %v", test.wantError, err)
@@ -135,60 +135,34 @@ func TestAddCommand(t *testing.T) {
135135
t.Fatal(err)
136136
}
137137

138-
testName := "google-cloud-secret-manager"
139138
for _, test := range []struct {
140139
name string
141-
args []string
142-
wantErr error
140+
apis []string
141+
wantName string
143142
wantAPIs []*config.API
143+
wantErr error
144144
}{
145145
{
146146
name: "no args",
147-
args: []string{"librarian", "add"},
148-
wantErr: errMissingLibraryName,
147+
wantErr: errMissingAPI,
149148
},
150149
{
151-
name: "library name only",
152-
args: []string{
153-
"librarian",
154-
"add",
155-
testName,
156-
},
150+
name: "single API",
151+
apis: []string{"google/cloud/secretmanager/v1"},
152+
wantName: "google-cloud-secretmanager-v1",
157153
},
158154
{
159-
name: "library with single API",
160-
args: []string{
161-
"librarian",
162-
"add",
163-
testName,
164-
"google/cloud/secretmanager/v1",
165-
},
166-
wantAPIs: []*config.API{
167-
{
168-
Path: "google/cloud/secretmanager/v1",
169-
},
170-
},
171-
},
172-
{
173-
name: "library with multiple APIs",
174-
args: []string{
175-
"librarian",
176-
"add",
177-
testName,
155+
name: "multiple APIs",
156+
apis: []string{
178157
"google/cloud/secretmanager/v1",
179158
"google/cloud/secretmanager/v1beta2",
180159
"google/cloud/secrets/v1beta1",
181160
},
161+
wantName: "google-cloud-secretmanager-v1",
182162
wantAPIs: []*config.API{
183-
{
184-
Path: "google/cloud/secretmanager/v1",
185-
},
186-
{
187-
Path: "google/cloud/secretmanager/v1beta2",
188-
},
189-
{
190-
Path: "google/cloud/secrets/v1beta1",
191-
},
163+
{Path: "google/cloud/secretmanager/v1"},
164+
{Path: "google/cloud/secretmanager/v1beta2"},
165+
{Path: "google/cloud/secrets/v1beta1"},
192166
},
193167
},
194168
} {
@@ -203,7 +177,8 @@ func TestAddCommand(t *testing.T) {
203177
if err := yaml.Write(librarianConfigPath, cfg); err != nil {
204178
t.Fatal(err)
205179
}
206-
err := Run(t.Context(), test.args...)
180+
args := append([]string{"librarian", "add"}, test.apis...)
181+
err := Run(t.Context(), args...)
207182
if test.wantErr != nil {
208183
if !errors.Is(err, test.wantErr) {
209184
t.Fatalf("want error %v, got %v", test.wantErr, err)
@@ -218,14 +193,12 @@ func TestAddCommand(t *testing.T) {
218193
if err != nil {
219194
t.Fatal(err)
220195
}
221-
got, err := findLibrary(gotCfg, testName)
196+
got, err := findLibrary(gotCfg, test.wantName)
222197
if err != nil {
223198
t.Fatal(err)
224199
}
225-
if test.wantAPIs != nil {
226-
if diff := cmp.Diff(test.wantAPIs, got.APIs); diff != "" {
227-
t.Errorf("apis mismatch (-want +got):\n%s", diff)
228-
}
200+
if diff := cmp.Diff(test.wantAPIs, got.APIs); diff != "" {
201+
t.Errorf("apis mismatch (-want +got):\n%s", diff)
229202
}
230203
})
231204
}
@@ -238,10 +211,6 @@ func TestAddLibraryToLibrarianYaml(t *testing.T) {
238211
apis []string
239212
want []*config.API
240213
}{
241-
{
242-
name: "library with no specification-source",
243-
libraryName: "newlib",
244-
},
245214
{
246215
name: "library with single API",
247216
libraryName: "newlib",
@@ -305,3 +274,27 @@ func TestAddLibraryToLibrarianYaml(t *testing.T) {
305274
})
306275
}
307276
}
277+
278+
func TestDeriveLibraryName(t *testing.T) {
279+
for _, test := range []struct {
280+
language string
281+
apiPath string
282+
want string
283+
}{
284+
{"python", "google/cloud/secretmanager/v1", "google-cloud-secretmanager"},
285+
{"python", "google/cloud/secretmanager/v1beta2", "google-cloud-secretmanager"},
286+
{"python", "google/cloud/storage/v2alpha", "google-cloud-storage"},
287+
{"python", "google/maps/addressvalidation/v1", "google-maps-addressvalidation"},
288+
{"python", "google/api/v1", "google-api"},
289+
{"rust", "google/cloud/secretmanager/v1", "google-cloud-secretmanager-v1"},
290+
{"rust", "google/cloud/secretmanager/v1beta2", "google-cloud-secretmanager-v1beta2"},
291+
{"fake", "google/cloud/secretmanager/v1", "google-cloud-secretmanager-v1"},
292+
} {
293+
t.Run(test.language+"/"+test.apiPath, func(t *testing.T) {
294+
got := deriveLibraryName(test.language, test.apiPath)
295+
if got != test.want {
296+
t.Errorf("deriveLibraryName(%q, %q) = %q, want %q", test.language, test.apiPath, got, test.want)
297+
}
298+
})
299+
}
300+
}

internal/librarian/fake.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,9 @@ func fakeCreateSkeleton(library *config.Library) error {
7979
starterPath := filepath.Join(library.Output, "STARTER.md")
8080
return os.WriteFile(starterPath, []byte(content), 0644)
8181
}
82+
83+
// fakeDefaultLibraryName derives a library name from an API path by
84+
// replacing "/" with "-".
85+
func fakeDefaultLibraryName(api string) string {
86+
return strings.ReplaceAll(api, "/", "-")
87+
}

internal/librarian/python/generate.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,16 @@ func cleanUpFilesAfterPostProcessing(repoRoot string) error {
297297
func DefaultOutputByName(name, defaultOutput string) string {
298298
return filepath.Join(defaultOutput, name)
299299
}
300+
301+
// DefaultLibraryName derives a library name from an API path by stripping
302+
// the version suffix and replacing "/" with "-".
303+
// For example: "google/cloud/secretmanager/v1" ->
304+
// "google-cloud-secretmanager".
305+
func DefaultLibraryName(api string) string {
306+
path := api
307+
if v := filepath.Base(api); len(v) > 1 && v[0] == 'v' && v[1] >= '0' && v[1] <= '9' {
308+
// Strip version suffix (v1, v1beta2, v2alpha, etc.).
309+
path = filepath.Dir(api)
310+
}
311+
return strings.ReplaceAll(path, "/", "-")
312+
}

internal/librarian/python/generate_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,27 @@ func TestDefaultOutputByName(t *testing.T) {
382382
}
383383
}
384384

385+
func TestDefaultLibraryName(t *testing.T) {
386+
for _, test := range []struct {
387+
api string
388+
want string
389+
}{
390+
{"google/cloud/secretmanager/v1", "google-cloud-secretmanager"},
391+
{"google/cloud/secretmanager/v1beta2", "google-cloud-secretmanager"},
392+
{"google/cloud/storage/v2alpha", "google-cloud-storage"},
393+
{"google/maps/addressvalidation/v1", "google-maps-addressvalidation"},
394+
{"google/api/v1", "google-api"},
395+
{"google/cloud/vision", "google-cloud-vision"},
396+
} {
397+
t.Run(test.api, func(t *testing.T) {
398+
got := DefaultLibraryName(test.api)
399+
if diff := cmp.Diff(test.want, got); diff != "" {
400+
t.Errorf("mismatch (-want +got):\n%s", diff)
401+
}
402+
})
403+
}
404+
}
405+
385406
func requirePythonModule(t *testing.T, module string) {
386407
t.Helper()
387408
cmd := exec.Command("python3", "-c", fmt.Sprintf("import %s", module))

internal/librarian/rust/generate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ func Keep(library *config.Library) ([]string, error) {
160160
return keep, nil
161161
}
162162

163-
// defaultLibraryName derives a library name from a api path.
163+
// DefaultLibraryName derives a library name from a api path.
164164
// For example: google/cloud/secretmanager/v1 -> google-cloud-secretmanager-v1.
165-
func defaultLibraryName(api string) string {
165+
func DefaultLibraryName(api string) string {
166166
return strings.ReplaceAll(api, "/", "-")
167167
}
168168

internal/librarian/rust/generate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ func TestDefaultLibraryName(t *testing.T) {
303303
},
304304
} {
305305
t.Run(test.name, func(t *testing.T) {
306-
got := defaultLibraryName(test.api)
306+
got := DefaultLibraryName(test.api)
307307
if diff := cmp.Diff(test.want, got); diff != "" {
308308
t.Errorf("mismatch (-want +got):\n%s", diff)
309309
}

internal/librarian/tidy.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ func tidyLibrary(cfg *config.Config, lib *config.Library) error {
7373
// Veneers are never generated, so ensure skip_generate is false.
7474
lib.SkipGenerate = false
7575
}
76-
for _, ch := range lib.APIs {
77-
if isDerivableAPIPath(cfg.Language, lib.Name, ch.Path) {
78-
ch.Path = ""
79-
}
76+
// Only remove derivable API paths when there's exactly one API.
77+
// When there are multiple APIs, preserve all of them.
78+
if len(lib.APIs) == 1 && isDerivableAPIPath(cfg.Language, lib.Name, lib.APIs[0].Path) {
79+
lib.APIs[0].Path = ""
8080
}
8181
lib.APIs = slices.DeleteFunc(lib.APIs, func(ch *config.API) bool {
8282
return ch.Path == ""

0 commit comments

Comments
 (0)