Skip to content

Commit 5435467

Browse files
authored
feat(internal/librarian): update create to support multiple APIs (#3423)
Update `librarian create` to accept one or more API paths: ``` librarian create <library> [apis...] ``` This allows languages such as Go and Python to generate libraries that span multiple channels. When provided, all listed API paths are used to populate the generated library. Examples: ``` librarian create google-cloud-secret-manager librarian create google-cloud-secret-manager \ google/cloud/secretmanager/v1 librarian create google-cloud-secret-manager \ google/cloud/secretmanager/v1 \ google/cloud/secretmanager/v1beta2 \ google/cloud/secrets/v1beta1 \ --output packages/google-cloud-secret-manager ```
1 parent 628f2f4 commit 5435467

7 files changed

Lines changed: 357 additions & 111 deletions

File tree

internal/librarian/create.go

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"errors"
2020
"fmt"
2121
"sort"
22-
"strings"
2322

2423
"github.com/googleapis/librarian/internal/config"
2524
"github.com/googleapis/librarian/internal/librarian/rust"
@@ -29,7 +28,6 @@ import (
2928

3029
var (
3130
errUnsupportedLanguage = errors.New("library creation is not supported for the specified language")
32-
errOutputFlagRequired = errors.New("output flag is required when default.output is not set in librarian.yaml")
3331
errMissingLibraryName = errors.New("must provide library name as argument to create a new library")
3432
errNoYaml = errors.New("unable to read librarian.yaml")
3533
)
@@ -38,30 +36,29 @@ func createCommand() *cli.Command {
3836
return &cli.Command{
3937
Name: "create",
4038
Usage: "create a new client library",
41-
UsageText: "librarian create <library> [flags]",
39+
UsageText: "librarian create <library> [apis...] [flags]",
4240
Flags: []cli.Flag{
43-
&cli.StringFlag{
44-
Name: "specification-source",
45-
Usage: "path to the specification source (e.g., google/cloud/secretmanager/v1)",
46-
},
4741
&cli.StringFlag{
4842
Name: "output",
4943
Usage: "output directory (optional, will be derived if not provided)",
5044
},
5145
},
5246
Action: func(ctx context.Context, c *cli.Command) error {
53-
name := c.Args().First()
47+
args := c.Args()
48+
name := args.First()
5449
if name == "" {
5550
return errMissingLibraryName
5651
}
57-
specSource := c.String("specification-source")
58-
output := c.String("output")
59-
return runCreate(ctx, name, specSource, output)
52+
var channels []string
53+
if len(args.Slice()) > 1 {
54+
channels = args.Slice()[1:]
55+
}
56+
return runCreate(ctx, name, c.String("output"), channels...)
6057
},
6158
}
6259
}
6360

64-
func runCreate(ctx context.Context, name, specSource, output string) error {
61+
func runCreate(ctx context.Context, name, output string, channel ...string) error {
6562
cfg, err := yaml.Read[config.Config](librarianConfigPath)
6663
if err != nil {
6764
return fmt.Errorf("%w: %v", errNoYaml, err)
@@ -72,10 +69,7 @@ func runCreate(ctx context.Context, name, specSource, output string) error {
7269
return runGenerate(ctx, false, name)
7370
}
7471
}
75-
if output, err = deriveOutput(output, cfg, name, specSource, cfg.Language); err != nil {
76-
return err
77-
}
78-
if err := addLibraryToLibrarianConfig(cfg, name, output, specSource); err != nil {
72+
if err := addLibraryToLibrarianConfig(cfg, name, output, channel...); err != nil {
7973
return err
8074
}
8175
switch cfg.Language {
@@ -90,37 +84,17 @@ func runCreate(ctx context.Context, name, specSource, output string) error {
9084
}
9185
}
9286

93-
func deriveOutput(output string, cfg *config.Config, libraryName string, specSource string, language string) (string, error) {
94-
if output != "" {
95-
return output, nil
96-
}
97-
if cfg.Default == nil || cfg.Default.Output == "" {
98-
return "", errOutputFlagRequired
99-
}
100-
switch language {
101-
case languageRust:
102-
if specSource != "" {
103-
return defaultOutput(language, specSource, cfg.Default.Output), nil
104-
}
105-
libOutputDir := strings.ReplaceAll(libraryName, "-", "/")
106-
return defaultOutput(language, libOutputDir, cfg.Default.Output), nil
107-
default:
108-
return defaultOutput(language, specSource, cfg.Default.Output), nil
109-
}
110-
}
111-
112-
func addLibraryToLibrarianConfig(cfg *config.Config, name, output, specificationSource string) error {
87+
func addLibraryToLibrarianConfig(cfg *config.Config, name, output string, channel ...string) error {
11388
lib := &config.Library{
11489
Name: name,
11590
Output: output,
11691
Version: "0.1.0",
11792
}
118-
if specificationSource != "" {
119-
lib.Channels = []*config.Channel{
120-
{
121-
Path: specificationSource,
122-
},
123-
}
93+
94+
for _, c := range channel {
95+
lib.Channels = append(lib.Channels, &config.Channel{
96+
Path: c,
97+
})
12498
}
12599
cfg.Libraries = append(cfg.Libraries, lib)
126100
sort.Slice(cfg.Libraries, func(i, j int) bool {

internal/librarian/create_test.go

Lines changed: 128 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestCreateLibrary(t *testing.T) {
8181
if err := yaml.Write(librarianConfigPath, cfg); err != nil {
8282
t.Fatal(err)
8383
}
84-
if err := runCreate(t.Context(), test.libName, "", test.output); err != nil {
84+
if err := runCreate(t.Context(), test.libName, test.output); err != nil {
8585
t.Fatal(err)
8686
}
8787

@@ -120,96 +120,142 @@ func TestCreateLibraryNoYaml(t *testing.T) {
120120
tmpDir := t.TempDir()
121121
t.Chdir(tmpDir)
122122

123-
err := runCreate(t.Context(), "newlib", "", "output/newlib")
123+
err := runCreate(t.Context(), "newlib", "output/newlib")
124124
if !errors.Is(err, errNoYaml) {
125125
t.Errorf("want error %v, got %v", errNoYaml, err)
126126
}
127127
}
128128

129129
func TestCreateCommand(t *testing.T) {
130+
googleapisDir, err := filepath.Abs("testdata/googleapis")
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
testName := "google-cloud-secret-manager"
130136
for _, test := range []struct {
131-
name string
132-
args []string
133-
wantErr error
137+
name string
138+
args []string
139+
wantErr error
140+
wantChannels []*config.Channel
141+
wantOutput string
134142
}{
135143
{
136144
name: "no args",
137145
args: []string{"librarian", "create"},
138146
wantErr: errMissingLibraryName,
139147
},
140-
} {
141-
t.Run(test.name, func(t *testing.T) {
142-
143-
err := Run(t.Context(), test.args...)
144-
if test.wantErr != nil {
145-
if !errors.Is(err, test.wantErr) {
146-
t.Errorf("want error %v, got %v", test.wantErr, err)
147-
}
148-
return
149-
}
150-
})
151-
}
152-
}
153-
154-
func TestDeriveOutput(t *testing.T) {
155-
for _, test := range []struct {
156-
name string
157-
specSource string
158-
output string
159-
defaultOutput string
160-
expectedOutput string
161-
libraryName string
162-
language string
163-
wantErr error
164-
}{
165-
166148
{
167-
name: "default rust output directory used with spec source",
168-
language: "rust",
169-
specSource: "google/cloud/storage/v1",
170-
defaultOutput: "default",
171-
expectedOutput: "default/cloud/storage/v1",
149+
name: "library name only",
150+
args: []string{
151+
"librarian",
152+
"create",
153+
"google-cloud-secretmanager-v1",
154+
},
172155
},
173156
{
174-
name: "default rust output directory used with default package",
175-
language: "rust",
176-
defaultOutput: "default",
177-
libraryName: "google-cloud-storage-v1",
178-
expectedOutput: "default/cloud/storage/v1",
157+
name: "library with single API",
158+
args: []string{
159+
"librarian",
160+
"create",
161+
testName,
162+
"google/cloud/secretmanager/v1",
163+
},
164+
wantChannels: []*config.Channel{
165+
{
166+
Path: "google/cloud/secretmanager/v1",
167+
},
168+
},
179169
},
180170
{
181-
name: "rust override output directory",
182-
language: "rust",
183-
output: "override",
184-
expectedOutput: "override",
171+
name: "library with multiple APIs",
172+
args: []string{
173+
"librarian",
174+
"create",
175+
testName,
176+
"google/cloud/secretmanager/v1",
177+
"google/cloud/secretmanager/v1beta2",
178+
"google/cloud/secrets/v1beta1",
179+
},
180+
wantChannels: []*config.Channel{
181+
{
182+
Path: "google/cloud/secretmanager/v1",
183+
},
184+
{
185+
Path: "google/cloud/secretmanager/v1beta2",
186+
},
187+
{
188+
Path: "google/cloud/secrets/v1beta1",
189+
},
190+
},
185191
},
186192
{
187-
name: "rust no default output directory",
188-
language: "rust",
189-
specSource: "google/cloud/storage/v1",
190-
libraryName: "google-cloud-storage-v1",
191-
wantErr: errOutputFlagRequired,
193+
name: "library with multiple APIs and output flag",
194+
args: []string{
195+
"librarian",
196+
"create",
197+
testName,
198+
"google/cloud/secretmanager/v1",
199+
"google/cloud/secretmanager/v1beta2",
200+
"google/cloud/secrets/v1beta1",
201+
"--output",
202+
"packages/google-cloud-secret-manager",
203+
},
204+
wantOutput: "packages/google-cloud-secret-manager",
205+
wantChannels: []*config.Channel{
206+
{
207+
Path: "google/cloud/secretmanager/v1",
208+
},
209+
{
210+
Path: "google/cloud/secretmanager/v1beta2",
211+
},
212+
{
213+
Path: "google/cloud/secrets/v1beta1",
214+
},
215+
},
192216
},
193217
} {
194218
t.Run(test.name, func(t *testing.T) {
219+
tmpDir := t.TempDir()
220+
t.Chdir(tmpDir)
221+
195222
cfg := &config.Config{
196-
Language: test.language,
223+
Language: languageFake,
224+
Default: &config.Default{
225+
Output: "output",
226+
},
227+
Sources: &config.Sources{
228+
Googleapis: &config.Source{
229+
Dir: googleapisDir,
230+
},
231+
},
197232
}
198-
if test.defaultOutput != "" {
199-
cfg.Default = &config.Default{Output: test.defaultOutput}
233+
if err := yaml.Write(librarianConfigPath, cfg); err != nil {
234+
t.Fatal(err)
200235
}
201-
got, err := deriveOutput(test.output, cfg, test.libraryName, test.specSource, test.language)
236+
err := Run(t.Context(), test.args...)
202237
if test.wantErr != nil {
203238
if !errors.Is(err, test.wantErr) {
204-
t.Errorf("want error %v, got %v", test.wantErr, err)
239+
t.Fatalf("want error %v, got %v", test.wantErr, err)
205240
}
206241
return
207242
}
208243
if err != nil {
209244
t.Fatal(err)
210245
}
211-
if got != test.expectedOutput {
212-
t.Errorf("want output %q, got %q", test.expectedOutput, got)
246+
247+
gotCfg, err := yaml.Read[config.Config](librarianConfigPath)
248+
if err != nil {
249+
t.Fatal(err)
250+
}
251+
got := findLibrary(gotCfg, testName)
252+
if test.wantOutput != "" && got.Output != test.wantOutput {
253+
t.Errorf("output = %q, want %q", got.Output, test.wantOutput)
254+
}
255+
if test.wantChannels != nil {
256+
if diff := cmp.Diff(test.wantChannels, got.Channels); diff != "" {
257+
t.Errorf("channels mismatch (-want +got):\n%s", diff)
258+
}
213259
}
214260
})
215261
}
@@ -220,7 +266,7 @@ func TestAddLibraryToLibrarianYaml(t *testing.T) {
220266
name string
221267
libraryName string
222268
output string
223-
specSource string
269+
channels []string
224270
want []*config.Channel
225271
}{
226272
{
@@ -229,16 +275,37 @@ func TestAddLibraryToLibrarianYaml(t *testing.T) {
229275
output: "output/newlib",
230276
},
231277
{
232-
name: "library with specification-source",
278+
name: "library with single API",
233279
libraryName: "newlib",
234280
output: "output/newlib",
235-
specSource: "google/cloud/storage/v1",
281+
channels: []string{"google/cloud/storage/v1"},
236282
want: []*config.Channel{
237283
{
238284
Path: "google/cloud/storage/v1",
239285
},
240286
},
241287
},
288+
{
289+
name: "library with multiple APIs",
290+
libraryName: "google-cloud-secret-manager",
291+
output: "output/google-cloud-secret-manager",
292+
channels: []string{
293+
"google/cloud/secretmanager/v1",
294+
"google/cloud/secretmanager/v1beta2",
295+
"google/cloud/secrets/v1beta1",
296+
},
297+
want: []*config.Channel{
298+
{
299+
Path: "google/cloud/secretmanager/v1",
300+
},
301+
{
302+
Path: "google/cloud/secretmanager/v1beta2",
303+
},
304+
{
305+
Path: "google/cloud/secrets/v1beta1",
306+
},
307+
},
308+
},
242309
} {
243310
t.Run(test.name, func(t *testing.T) {
244311
tmpDir := t.TempDir()
@@ -256,7 +323,7 @@ func TestAddLibraryToLibrarianYaml(t *testing.T) {
256323
if err := yaml.Write(librarianConfigPath, cfg); err != nil {
257324
t.Fatal(err)
258325
}
259-
if err := addLibraryToLibrarianConfig(cfg, test.libraryName, test.output, test.specSource); err != nil {
326+
if err := addLibraryToLibrarianConfig(cfg, test.libraryName, test.output, test.channels...); err != nil {
260327
t.Fatal(err)
261328
}
262329

0 commit comments

Comments
 (0)