Skip to content

Commit 90e5ac8

Browse files
authored
feat(internal/surfer): CLI infrastructure (#3293)
This pull request updates the `surfer cli` by adding a parameter to support the additional requirements for parsing and generating gcloud files. As the final request in the PR stack, it should be merged and reviewed last. > Note: This pull request is one of several resulting from the breakdown of the [ feat-generate-parallelstore-poc ](https://github.com/quirogas/librarian/tree/feat-generate-parallelstore-poc) branch into smaller, atomic pull requests for easier review and integration. This PR is staked, meaning it points to previous work and is dependent on it. stack: #3293 (this pr) <- #3296 <- #3295 <- #3294 ## Changes - Migrates `cmd/surfer/main.go` and `internal/surfer/surfer/` to the new CLI framework. - Moves documentation to `doc/surfer.md`. - Removes legacy setup scripts. - Updates `go.mod`. ## Related Issues Updates: #2817
1 parent 946c068 commit 90e5ac8

28 files changed

Lines changed: 2223 additions & 112 deletions

internal/surfer/gcloud/builder.go

Lines changed: 411 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 gcloud
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
"github.com/google/go-cmp/cmp/cmpopts"
22+
"github.com/googleapis/librarian/internal/sidekick/api"
23+
)
24+
25+
func TestNewParam(t *testing.T) {
26+
// Helper to create a basic field
27+
makeField := func(name string, typez api.Typez) *api.Field {
28+
return &api.Field{
29+
Name: name,
30+
JSONName: name, // simplify default
31+
Typez: typez,
32+
Behavior: []api.FieldBehavior{api.FIELD_BEHAVIOR_OPTIONAL},
33+
}
34+
}
35+
36+
for _, test := range []struct {
37+
name string
38+
field *api.Field
39+
apiField string
40+
want Param
41+
wantErr bool
42+
}{
43+
{
44+
name: "String Field",
45+
field: makeField("description", api.STRING_TYPE),
46+
apiField: "description",
47+
want: Param{
48+
ArgName: "description",
49+
APIField: "description",
50+
Type: "str", // String is default/empty
51+
HelpText: "Value for the `description` field.",
52+
Required: false,
53+
Repeated: false,
54+
},
55+
},
56+
{
57+
name: "Int Field",
58+
field: makeField("capacity_gib", api.INT64_TYPE),
59+
apiField: "capacityGib",
60+
want: Param{
61+
ArgName: "capacity-gib",
62+
APIField: "capacityGib",
63+
Type: "int",
64+
HelpText: "Value for the `capacity-gib` field.",
65+
Required: false,
66+
Repeated: false,
67+
},
68+
},
69+
{
70+
name: "Repeated Field",
71+
field: &api.Field{
72+
Name: "labels",
73+
JSONName: "labels",
74+
Typez: api.STRING_TYPE,
75+
Repeated: true,
76+
},
77+
apiField: "labels",
78+
want: Param{
79+
ArgName: "labels",
80+
APIField: "labels",
81+
Type: "str",
82+
HelpText: "Value for the `labels` field.",
83+
Required: false,
84+
Repeated: true,
85+
},
86+
},
87+
{
88+
name: "Required Field",
89+
field: &api.Field{
90+
Name: "name",
91+
JSONName: "name",
92+
Typez: api.STRING_TYPE,
93+
Behavior: []api.FieldBehavior{api.FIELD_BEHAVIOR_REQUIRED},
94+
},
95+
apiField: "name",
96+
want: Param{
97+
ArgName: "name",
98+
APIField: "name",
99+
Type: "str",
100+
HelpText: "Value for the `name` field.",
101+
Required: true,
102+
Repeated: false,
103+
},
104+
},
105+
} {
106+
t.Run(test.name, func(t *testing.T) {
107+
got, err := newParam(test.field, test.apiField, &Config{}, &api.API{}, &api.Service{})
108+
if (err != nil) != test.wantErr {
109+
t.Errorf("newParam() error = %v, wantErr %v", err, test.wantErr)
110+
return
111+
}
112+
// Ignore fields that are hard to mock or irrelevant for basic mapping test
113+
if diff := cmp.Diff(test.want, got, cmpopts.IgnoreFields(Param{}, "ResourceSpec")); diff != "" {
114+
t.Errorf("newParam() mismatch (-want +got):\n%s", diff)
115+
}
116+
})
117+
}
118+
}

internal/surfer/gcloud/command.go

Lines changed: 181 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,82 +14,209 @@
1414

1515
package gcloud
1616

17-
import "github.com/googleapis/librarian/internal/yaml"
18-
19-
// Command is a single gcloud command.
17+
// Command represents the top-level structure for a gcloud command definition.
18+
// This struct is designed to be marshaled into a YAML file that the gcloud generator can
19+
// understand and use to generate a command-line interface.
2020
type Command struct {
21-
ReleaseTracks []ReleaseTrack `yaml:"release_tracks,omitempty"`
22-
Autogenerated bool `yaml:"auto_generated,omitempty"`
23-
Hidden bool `yaml:"hidden,omitempty"`
24-
HelpText *CommandHelpText `yaml:"help_text,omitempty"`
25-
Arguments *Arguments `yaml:"arguments,omitempty"`
26-
Request *Request `yaml:"request,omitempty"`
27-
Response *Response `yaml:"response,omitempty"`
28-
Async *Async `yaml:"async,omitempty"`
21+
// ReleaseTracks specifies the release tracks (e.g., GA, BETA, ALPHA) for which
22+
// this command is available.
23+
// Origin: Derived from the `release_tracks` field in the `gcloud.yaml` config file.
24+
ReleaseTracks []string `yaml:"release_tracks,omitempty"`
25+
26+
// AutoGenerated indicates that this command was generated by a tool.
27+
// Origin: Hardcoded to `true` by this generator.
28+
AutoGenerated bool `yaml:"auto_generated,omitempty"`
29+
30+
// Hidden specifies whether this command should be hidden from the user in
31+
// help text and command listings.
32+
// Origin: Hardcoded to `true` by this generator.
33+
Hidden bool `yaml:"hidden,omitempty"`
34+
35+
// HelpText contains the brief and detailed help text for the command.
36+
// Origin: Populated from `method_rules` in the `gcloud.yaml` config file.
37+
HelpText HelpText `yaml:"help_text"`
38+
39+
// Arguments defines the set of flags and positional arguments for the command.
40+
// Origin: Generated by parsing the fields of the method's request message from the proto.
41+
Arguments Arguments `yaml:"arguments"`
42+
43+
// Request specifies the details of the API request to be made when the
44+
// command is executed.
45+
// Origin: Generated by `newRequest`, which determines the API version and collection.
46+
Request *Request `yaml:"request,omitempty"`
47+
48+
// Async specifies the configuration for handling long-running operations.
49+
// Origin: Generated by `newAsync` if the proto method is annotated as a long-running operation.
50+
Async *Async `yaml:"async,omitempty"`
51+
52+
// Response specifies the details of the API response.
53+
Response *Response `yaml:"response,omitempty"`
54+
}
55+
56+
// Response defines the details of the API response.
57+
type Response struct {
58+
// IDField is the name of the field in the response that contains the resource ID.
59+
IDField string `yaml:"id_field,omitempty"`
60+
}
61+
62+
// HelpText holds the brief and detailed help text for the command.
63+
// It maps to the `help_text` section of the gcloud YAML schema.
64+
type HelpText struct {
65+
// Brief is a short, one-line summary of what the command does.
66+
// Origin: From the `brief` field in a `method_rule` in `gcloud.yaml`.
67+
Brief string `yaml:"brief"`
68+
// Description is a more detailed explanation of the command's functionality.
69+
// Origin: From the `description` field in a `method_rule` in `gcloud.yaml`.
70+
Description string `yaml:"description"`
71+
// Examples provides one or more examples of how to use the command.
72+
// Origin: From the `examples` field in a `method_rule` in `gcloud.yaml`.
73+
Examples string `yaml:"examples,omitempty"`
2974
}
3075

31-
// Arguments are the arguments for a gcloud command.
76+
// Arguments contains the list of parameters (flags and positionals) for the command.
77+
// It maps to the `arguments` section of the gcloud YAML schema.
3278
type Arguments struct {
33-
Params []*Param `yaml:"params,omitempty"`
79+
// Params is a slice of Param structs, each defining a single argument.
80+
// Origin: Generated by iterating over the fields of a method's request message.
81+
Params []Param `yaml:"params,omitempty"`
3482
}
3583

36-
// Param is a single parameter for a gcloud command.
84+
// Param represents a single command-line argument or flag.
85+
// It maps to an item in the `arguments.params` list in the gcloud YAML schema.
3786
type Param struct {
38-
ArgName string `yaml:"arg_name,omitempty"`
39-
APIField string `yaml:"api_field,omitempty"`
40-
Type string `yaml:"type,omitempty"`
41-
Repeated bool `yaml:"repeated,omitempty"`
42-
HelpText string `yaml:"help_text,omitempty"`
43-
IsPositional bool `yaml:"is_positional,omitempty"`
44-
IsPrimaryResource bool `yaml:"is_primary_resource,omitempty"`
45-
RequestIdField string `yaml:"request_id_field,omitempty"`
46-
ResourceSpec yaml.RefString `yaml:"resource_spec,omitempty"`
47-
Required bool `yaml:"required,omitempty"`
87+
// ArgName is the name of the argument as it appears on the command line
88+
// (e.g., "instance-id").
89+
// Origin: Derived from the proto field name, converted to kebab-case.
90+
// TODO(https://github.com/googleapis/librarian/issues/3287): Support arg groups.
91+
// TODO(https://github.com/googleapis/librarian/issues/3288): Handle arg name collisions with path prefixes.
92+
ArgName string `yaml:"arg_name,omitempty"`
93+
// APIField is the dot-separated path to the field in the API request message
94+
// that this argument's value should be placed in (e.g., "instance.name").
95+
// Origin: Derived from the `json_name` of the proto field, with prefixes for nested messages.
96+
APIField string `yaml:"api_field,omitempty"`
97+
// HelpText is the help text for this specific argument.
98+
// Origin: From a `field_rule` in `gcloud.yaml`, or a default value is generated if none is provided.
99+
HelpText string `yaml:"help_text"`
100+
// IsPositional indicates that this argument is a positional argument rather
101+
// than a flag.
102+
// Origin: Set to `true` for the primary resource argument of a command.
103+
IsPositional bool `yaml:"is_positional,omitempty"`
104+
// IsPrimaryResource indicates that this argument represents the primary
105+
// resource being acted upon by the command.
106+
// Origin: Set to `true` for the primary resource argument, identified by its field name (e.g., "name").
107+
IsPrimaryResource bool `yaml:"is_primary_resource,omitempty"`
108+
// RequestIDField is the name of the field in the request message that should
109+
// hold the ID of the resource being created. This is used for `Create` methods.
110+
// Origin: Derived from the name of the primary resource's ID field (e.g., "instance_id").
111+
RequestIDField string `yaml:"request_id_field,omitempty"`
112+
// ResourceSpec defines the structure of a resource argument, including its
113+
// name, collection, and attributes.
114+
// Origin: Generated for fields that have a `(google.api.resource_reference)` annotation.
115+
ResourceSpec *ResourceSpec `yaml:"resource_spec,omitempty"`
116+
// Required indicates that this argument must be provided by the user.
117+
// Origin: Inferred from the `(google.api.field_behavior) = REQUIRED` annotation on the proto field.
118+
Required bool `yaml:"required,omitempty"`
119+
// Repeated indicates that this argument can be specified multiple times.
120+
// Origin: Inferred from the `repeated` keyword on the proto field.
121+
Repeated bool `yaml:"repeated,omitempty"`
122+
// Type specifies the data type of the argument's value (e.g., "long", "float").
123+
// Origin: Mapped from the proto field's data type (e.g., `int64` becomes `long`).
124+
Type string `yaml:"type,omitempty"`
125+
// Choices is a list of valid values for an enum-based argument.
126+
// Origin: Generated by iterating over the values of a proto `enum`.
127+
Choices []Choice `yaml:"choices,omitempty"`
128+
// Spec defines the structure for complex argument types, such as key-value pairs for a map.
129+
// Origin: Generated for `map` fields in a proto message.
130+
Spec []ArgSpec `yaml:"spec,omitempty"`
131+
// ResourceMethodParams maps API method parameters to resource attributes,
132+
// used for non-standard resource name formats.
133+
// Origin: Generated for resource reference arguments to map the parsed name correctly.
48134
ResourceMethodParams map[string]string `yaml:"resource_method_params,omitempty"`
49-
Spec []*FieldSpec `yaml:"spec,omitempty"`
50-
Choices []*Choice `yaml:"choices,omitempty"`
51135
}
52136

53-
// FieldSpec is a specification for a field.
54-
type FieldSpec struct {
137+
// ArgSpec defines the structure within a complex argument type, such as the
138+
// key and value fields for a map.
139+
type ArgSpec struct {
140+
// APIField is the name of the field in the API message (e.g., "key", "value").
141+
// Origin: Hardcoded to "key" and "value" for map fields.
55142
APIField string `yaml:"api_field,omitempty"`
56143
}
57144

58-
// Choice is a choice for a parameter.
145+
// Choice represents a single option for an enum-based argument.
146+
// It maps to an item in the `choices` list for a parameter.
59147
type Choice struct {
60-
ArgValue string `yaml:"arg_value,omitempty"`
148+
// ArgValue is the value as it appears on the command line (e.g., "balanced").
149+
// Origin: Derived from the proto enum value name, converted to kebab-case.
150+
ArgValue string `yaml:"arg_value,omitempty"`
151+
// EnumValue is the corresponding string value of the enum in the API.
152+
// Origin: The original name of the value in the proto enum definition.
61153
EnumValue string `yaml:"enum_value,omitempty"`
62-
HelpText string `yaml:"help_text,omitempty"`
63154
}
64155

65-
// Request is the request for a gcloud command.
66-
type Request struct {
67-
ALPHA *TrackInfo `yaml:"ALPHA,omitempty"`
68-
BETA *TrackInfo `yaml:"BETA,omitempty"`
69-
GA *TrackInfo `yaml:"GA,omitempty"`
70-
APIVersion string `yaml:"api_version,omitempty"`
71-
Collection []string `yaml:"collection,omitempty"`
72-
Method string `yaml:"method,omitempty"`
156+
// ResourceSpec defines the structure for a gcloud resource argument. It provides
157+
// gcloud with the information it needs to parse and handle resource names, and
158+
// it maps to the `resource_spec` section in the gcloud YAML schema.
159+
type ResourceSpec struct {
160+
// Name is the singular name of the resource (e.g., "instance").
161+
// Origin: Inferred from the last variable segment of a resource pattern (e.g., `{instance}`).
162+
Name string `yaml:"name,omitempty"`
163+
// PluralName is the plural name of the resource (e.g., "instances").
164+
// Origin: Derived from the `plural` field in the `(google.api.resource)` annotation, or
165+
// inferred from the literal segment preceding the final variable in the resource pattern.
166+
PluralName string `yaml:"plural_name,omitempty"`
167+
// Collection is the unique identifier for the resource type in gcloud's
168+
// resource registry (e.g., "parallelstore.projects.locations.instances").
169+
// Origin: Constructed from the API's service name and the literal collection identifiers
170+
// in the resource's pattern string, as defined by AIP-122.
171+
Collection string `yaml:"collection,omitempty"`
172+
// Attributes defines the components that make up a resource's unique name.
173+
// Origin: Generated by parsing the variable segments (e.g., `{project}`, `{location}`)
174+
// from the resource's pattern string.
175+
Attributes []Attribute `yaml:"attributes,omitempty"`
176+
// DisableAutoCompleters prevents gcloud from attempting to provide tab-completion
177+
// for this resource.
178+
// Origin: Hardcoded to `true` for referenced resources to avoid cross-API complexities.
179+
DisableAutoCompleters bool `yaml:"disable_auto_completers,omitempty"`
73180
}
74181

75-
// Async is the async information for a gcloud command.
76-
type Async struct {
77-
Collection []string `yaml:"collection,omitempty"`
182+
// Attribute defines a single component of a resource's identifier, such as a
183+
// project ID or a location. It maps to an item in the `attributes` list of a `resource_spec`.
184+
type Attribute struct {
185+
// ParameterName is the name of the API field in the request path that this
186+
// attribute maps to (e.g., "projectsId").
187+
// Origin: Inferred from the literal collection identifier that precedes the variable
188+
// in the resource pattern (e.g., `projects/{project}` -> `projectsId`).
189+
ParameterName string `yaml:"parameter_name,omitempty"`
190+
// AttributeName is the name used for the gcloud flag for this attribute
191+
// (e.g., "project").
192+
// Origin: Inferred from the name of the variable in the resource pattern (e.g., `{project}`).
193+
AttributeName string `yaml:"attribute_name,omitempty"`
194+
// Help is the help text for the gcloud flag for this attribute.
195+
// Origin: Auto-generated using a standard template.
196+
Help string `yaml:"help,omitempty"`
197+
// Property is a gcloud core property that can be used as a fallback value
198+
// if the flag is not provided (e.g., "core/project").
199+
// Origin: Hardcoded for standard, well-known attributes like "project".
200+
Property string `yaml:"property,omitempty"`
78201
}
79202

80-
// TrackInfo is the track information for a gcloud command.
81-
type TrackInfo struct {
203+
// Request defines the API request details for the command.
204+
// It maps to the `request` section of the gcloud YAML schema.
205+
type Request struct {
206+
// APIVersion is the version of the API to call (e.g., "v1").
207+
// Origin: Derived from the `api_version` field in the `gcloud.yaml` config file.
82208
APIVersion string `yaml:"api_version,omitempty"`
209+
// Collection is the list of API collections that this command operates on.
210+
// Origin: Constructed from the API service name and the resource's collection path.
211+
Collection []string `yaml:"collection,omitempty"`
212+
// Method is the name of the API method to call.
213+
Method string `yaml:"method,omitempty"`
83214
}
84215

85-
// Response is the response for a gcloud command.
86-
type Response struct {
87-
IDField string `yaml:"id_field,omitempty"`
88-
}
89-
90-
// CommandHelpText is the help text for a gcloud command.
91-
type CommandHelpText struct {
92-
Brief string `yaml:"brief,omitempty"`
93-
Description string `yaml:"description,omitempty"`
94-
Examples string `yaml:"examples,omitempty"`
216+
// Async defines the details for handling long-running operations.
217+
// It maps to the `async` section of the gcloud YAML schema.
218+
type Async struct {
219+
// Collection is the API collection for the long-running operation resource.
220+
// Origin: Hardcoded to the standard operations collection for the service.
221+
Collection []string `yaml:"collection,omitempty"`
95222
}

0 commit comments

Comments
 (0)