Skip to content

Commit e7a679a

Browse files
authored
feat(surfer): create surfer generate command framework (#2834)
Create the initial surfer CLI and add the framework for `surfer generate`. The generation logic will be added in a follow-up change. Remove gcloud.Generate from sidekick, since we no longer intend to run it from the sidekick CLI. Move `testdata/gcloud.yaml` to `testdata/parallelstore/gcloud.yaml` to make it clear that it belongs to the parallelstore service. For #2375
1 parent b4e4003 commit e7a679a

File tree

7 files changed

+213
-16
lines changed

7 files changed

+213
-16
lines changed

cmd/surfer/main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
15+
// Command surfer generates gcloud command YAML files.
16+
package main
17+
18+
import (
19+
"context"
20+
"log"
21+
"os"
22+
23+
"github.com/googleapis/librarian/internal/surfer/surfer"
24+
)
25+
26+
func main() {
27+
ctx := context.Background()
28+
if err := surfer.Run(ctx, os.Args[1:]); err != nil {
29+
log.Fatal(err)
30+
}
31+
}

internal/sidekick/sidekick/refresh.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/googleapis/librarian/internal/sidekick/parser"
2626
"github.com/googleapis/librarian/internal/sidekick/rust"
2727
"github.com/googleapis/librarian/internal/sidekick/rust_prost"
28-
"github.com/googleapis/librarian/internal/surfer/gcloud"
2928
)
3029

3130
func init() {
@@ -99,8 +98,6 @@ func refreshDir(rootConfig *config.Config, cmdLine *CommandLine, output string)
9998
return dart.Generate(model, output, config)
10099
case "sample":
101100
return codec_sample.Generate(model, output, config)
102-
case "gcloud":
103-
return gcloud.Generate(model, output, config)
104101
default:
105102
return fmt.Errorf("unknown language: %s", config.General.Language)
106103
}

internal/surfer/gcloud/config_test.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,24 @@ import (
2525
"gopkg.in/yaml.v3"
2626
)
2727

28-
func TestGcloudConfig(t *testing.T) {
29-
data, err := os.ReadFile("testdata/gcloud.yaml")
28+
func TestReadGcloudConfig(t *testing.T) {
29+
cfg, err := readGcloudConfig("testdata/parallelstore/gcloud.yaml")
3030
if err != nil {
31-
t.Fatalf("failed to read temporary YAML file: %v", err)
32-
}
33-
34-
var config Config
35-
if err := yaml.Unmarshal(data, &config); err != nil {
36-
t.Fatalf("failed to unmarshal YAML: %v", err)
31+
t.Fatal(err)
3732
}
3833

3934
var got bytes.Buffer
4035
enc := yaml.NewEncoder(&got)
4136
enc.SetIndent(2)
42-
if err := enc.Encode(config); err != nil {
37+
if err := enc.Encode(cfg); err != nil {
4338
t.Fatalf("failed to marshal struct to YAML: %v", err)
4439
}
4540

4641
var index int
42+
data, err := os.ReadFile("testdata/parallelstore/gcloud.yaml")
43+
if err != nil {
44+
t.Fatalf("failed to read temporary YAML file: %v", err)
45+
}
4746
lines := strings.Split(string(data), "\n")
4847
for i, line := range lines {
4948
if strings.HasPrefix(line, "#") {
@@ -52,7 +51,7 @@ func TestGcloudConfig(t *testing.T) {
5251
continue
5352
}
5453
}
55-
want := fmt.Sprintf("service_name: %s\n%s", config.ServiceName, strings.Join(lines[index:], "\n"))
54+
want := fmt.Sprintf("service_name: %s\n%s", cfg.ServiceName, strings.Join(lines[index:], "\n"))
5655
if diff := cmp.Diff(want, got.String()); diff != "" {
5756
t.Errorf("mismatch(-want, +got)\n%s", diff)
5857
}

internal/surfer/gcloud/generate.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,52 @@
1515
package gcloud
1616

1717
import (
18-
"github.com/googleapis/librarian/internal/sidekick/api"
18+
"context"
19+
"fmt"
20+
"os"
21+
1922
"github.com/googleapis/librarian/internal/sidekick/config"
23+
"github.com/googleapis/librarian/internal/sidekick/parser"
24+
"gopkg.in/yaml.v3"
2025
)
2126

22-
// Generate generates gcloud commands from the model.
23-
func Generate(model *api.API, outdir string, cfg *config.Config) error {
27+
// Generate generates gcloud commands for a service.
28+
func Generate(ctx context.Context, googleapis, gcloudconfig, output string) error {
29+
cfg, err := readGcloudConfig(gcloudconfig)
30+
if err != nil {
31+
return err
32+
}
33+
34+
model, err := parser.ParseProtobuf(&config.Config{
35+
General: config.GeneralConfig{
36+
// TODO(https://github.com/googleapis/librarian/issues/2817):
37+
// determine the specification source
38+
SpecificationSource: "",
39+
},
40+
Source: map[string]string{
41+
"googleapis-root": googleapis,
42+
},
43+
})
44+
if err != nil {
45+
return fmt.Errorf("failed to create API model: %w", err)
46+
}
47+
48+
// TODO(https://github.com/googleapis/librarian/issues/2817): implement
49+
// gcloud command generation logic
50+
_, _ = model, cfg
2451
return nil
2552
}
53+
54+
// readGcloudConfig loads the gcloud configuration from a gcloud.yaml file.
55+
func readGcloudConfig(path string) (*Config, error) {
56+
data, err := os.ReadFile(path)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to read gcloud config file: %w", err)
59+
}
60+
61+
var cfg Config
62+
if err := yaml.Unmarshal(data, &cfg); err != nil {
63+
return nil, fmt.Errorf("failed to parse gcloud config YAML: %w", err)
64+
}
65+
return &cfg, nil
66+
}
File renamed without changes.

internal/surfer/surfer/surfer.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
15+
// Package surfer provides the core implementation for the surfer CLI tool.
16+
package surfer
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
"github.com/googleapis/librarian/internal/cli"
23+
"github.com/googleapis/librarian/internal/surfer/gcloud"
24+
)
25+
26+
// Run executes the surfer CLI with the given command line arguments.
27+
func Run(ctx context.Context, args []string) error {
28+
cmd := &cli.Command{
29+
Short: "surfer generates gcloud command YAML files",
30+
UsageLine: "surfer generate [arguments]",
31+
Long: "surfer generates gcloud command YAML files",
32+
Commands: []*cli.Command{
33+
newCmdGenerate(),
34+
},
35+
}
36+
cmd.Init()
37+
return cmd.Run(ctx, args)
38+
}
39+
40+
func newCmdGenerate() *cli.Command {
41+
var (
42+
googleapis string
43+
out string
44+
)
45+
46+
cmdGenerate := &cli.Command{
47+
Short: "generate generates gcloud commands",
48+
UsageLine: "surfer generate <path to gcloud.yaml> --googleapis <path> [--out <path>]",
49+
Long: `generate generates gcloud commands
50+
51+
generate generates gcloud command files from protobuf API specifications,
52+
service config yaml, and gcloud.yaml.`,
53+
Action: func(ctx context.Context, cmd *cli.Command) error {
54+
args := cmd.Flags.Args()
55+
if len(args) == 0 {
56+
return fmt.Errorf("path to gcloud.yaml is required")
57+
}
58+
config := args[0]
59+
return gcloud.Generate(ctx, googleapis, config, out)
60+
},
61+
}
62+
cmdGenerate.Init()
63+
cmdGenerate.Flags.StringVar(&googleapis, "googleapis", "https://github.com/googleapis/googleapis", "URL or directory path to googleapis")
64+
cmdGenerate.Flags.StringVar(&out, "out", ".", "output directory")
65+
return cmdGenerate
66+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
15+
package surfer
16+
17+
import (
18+
"strings"
19+
"testing"
20+
)
21+
22+
func TestRun(t *testing.T) {
23+
for _, test := range []struct {
24+
name string
25+
args []string
26+
wantErr bool
27+
}{
28+
{
29+
name: "valid command",
30+
args: []string{
31+
"generate",
32+
"../gcloud/testdata/parallelstore/gcloud.yaml",
33+
"--out", "../gcloud/testdata/parallelstore/surface",
34+
},
35+
},
36+
{
37+
name: "invalid gcloud.yaml filepath",
38+
args: []string{
39+
"generate",
40+
"invalidpath/gcloud.yaml",
41+
},
42+
wantErr: true,
43+
},
44+
{
45+
name: "missing config arg",
46+
args: []string{"generate"},
47+
wantErr: true,
48+
},
49+
} {
50+
t.Run(test.name, func(t *testing.T) {
51+
if err := Run(t.Context(), test.args); err != nil {
52+
// TODO(https://github.com/googleapis/librarian/issues/2817):
53+
// remove once the generate functionality has been implemented
54+
if strings.Contains(err.Error(), "failed to create API model") {
55+
return
56+
}
57+
if !test.wantErr {
58+
t.Fatal(err)
59+
}
60+
}
61+
})
62+
}
63+
}

0 commit comments

Comments
 (0)