Skip to content

Commit a04acb7

Browse files
Codelaxremyleone
andauthored
feat(qa): add argspec checking in cli qa (#2852)
Co-authored-by: Rémy Léone <rleone@scaleway.com>
1 parent cb09597 commit a04acb7

File tree

6 files changed

+139
-71
lines changed

6 files changed

+139
-71
lines changed

internal/args/args.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,66 @@ func GetArgType(argType reflect.Type, name string) (reflect.Type, error) {
255255

256256
return recursiveFunc(argType, strings.Split(name, "."))
257257
}
258+
259+
var listArgTypeFieldsSkippedArguments = []string{
260+
"page",
261+
"page-size",
262+
"per-page",
263+
}
264+
265+
func listArgTypeFields(base string, argType reflect.Type) []string {
266+
if argType.Kind() != reflect.Ptr {
267+
// Can be a handled type like time.Time
268+
// If so, use it like a scalar type
269+
_, isHandled := unmarshalFuncs[argType]
270+
if isHandled {
271+
return []string{base}
272+
}
273+
}
274+
275+
switch argType.Kind() {
276+
case reflect.Ptr:
277+
return listArgTypeFields(base, argType.Elem())
278+
279+
case reflect.Slice:
280+
return listArgTypeFields(base+"."+sliceSchema, argType.Elem())
281+
282+
case reflect.Map:
283+
return listArgTypeFields(base+"."+mapSchema, argType.Elem())
284+
285+
case reflect.Struct:
286+
fields := []string(nil)
287+
288+
for i := 0; i < argType.NumField(); i++ {
289+
field := argType.Field(i)
290+
fieldBase := base
291+
292+
// If this is an embedded struct, skip adding its name to base
293+
if field.Anonymous {
294+
fields = append(fields, listArgTypeFields(fieldBase, field.Type)...)
295+
continue
296+
}
297+
298+
if fieldBase == "" {
299+
fieldBase = strcase.ToBashArg(field.Name)
300+
} else {
301+
fieldBase += "." + strcase.ToBashArg(field.Name)
302+
}
303+
fields = append(fields, listArgTypeFields(fieldBase, field.Type)...)
304+
}
305+
306+
return fields
307+
default:
308+
for _, skippedArg := range listArgTypeFieldsSkippedArguments {
309+
if base == skippedArg {
310+
return []string{}
311+
}
312+
}
313+
return []string{base}
314+
}
315+
}
316+
317+
// ListArgTypeFields take a go struct and return a list of name that comply with ArgSpec name notation (e.g "friends.{index}.name")
318+
func ListArgTypeFields(argType reflect.Type) []string {
319+
return listArgTypeFields("", argType)
320+
}

internal/core/arg_specs.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package core
33
import (
44
"context"
55
"fmt"
6-
"reflect"
76
"strings"
87

98
"github.com/scaleway/scaleway-sdk-go/scw"
10-
"github.com/scaleway/scaleway-sdk-go/strcase"
119
"github.com/scaleway/scaleway-sdk-go/validation"
1210
)
1311

@@ -121,13 +119,6 @@ func (a *ArgSpec) ConflictWith(b *ArgSpec) bool {
121119
(a.OneOfGroup == b.OneOfGroup)
122120
}
123121

124-
// GetArgsTypeField returns the type of the argument in the given ArgsType
125-
func (a *ArgSpec) GetArgsTypeField(argsType reflect.Type) (reflect.Type, error) {
126-
argSpecGoName := strcase.ToPublicGoName(a.Name)
127-
128-
return getTypeForFieldByName(argsType, strings.Split(argSpecGoName, "."))
129-
}
130-
131122
type DefaultFunc func(ctx context.Context) (value string, doc string)
132123

133124
func ZoneArgSpec(zones ...scw.Zone) *ArgSpec {

internal/core/arg_specs_test.go

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package core
22

33
import (
4-
"reflect"
54
"testing"
65

76
"github.com/alecthomas/assert"
@@ -37,35 +36,3 @@ func TestOneOf(t *testing.T) {
3736
assert.False(t, a.ConflictWith(c))
3837
assert.False(t, e.ConflictWith(e))
3938
}
40-
41-
func TestArgSpecGetArgsTypeField(t *testing.T) {
42-
data := struct {
43-
Field string
44-
FieldStruct struct {
45-
NestedField int
46-
}
47-
FieldSlice []float32
48-
FieldMap map[string]bool
49-
}{}
50-
dataType := reflect.TypeOf(data)
51-
52-
fieldSpec := ArgSpec{Name: "field"}
53-
typ, err := fieldSpec.GetArgsTypeField(dataType)
54-
assert.Nil(t, err)
55-
assert.Equal(t, reflect.TypeOf("string"), typ, "%s is not string", typ.Name())
56-
57-
fieldSpec = ArgSpec{Name: "field-struct.nested-field"}
58-
typ, err = fieldSpec.GetArgsTypeField(dataType)
59-
assert.Nil(t, err)
60-
assert.Equal(t, reflect.TypeOf(int(1)), typ, "%s is not int", typ.Name())
61-
62-
fieldSpec = ArgSpec{Name: "field-slice.{index}"}
63-
typ, err = fieldSpec.GetArgsTypeField(dataType)
64-
assert.Nil(t, err)
65-
assert.Equal(t, reflect.TypeOf(float32(1)), typ, "%s is not float32", typ.Name())
66-
67-
fieldSpec = ArgSpec{Name: "field-map.{key}"}
68-
typ, err = fieldSpec.GetArgsTypeField(dataType)
69-
assert.Nil(t, err)
70-
assert.Equal(t, reflect.TypeOf(true), typ, "%s is not bool", typ.Name())
71-
}

internal/core/reflect.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -109,32 +109,3 @@ func getValuesForFieldByName(value reflect.Value, parts []string) (values []refl
109109

110110
return nil, fmt.Errorf("case is not handled")
111111
}
112-
113-
// getTypeForFieldByName recursively search for fields in an ArgsType
114-
// The search is based on the name of the field.
115-
func getTypeForFieldByName(value reflect.Type, parts []string) (reflect.Type, error) {
116-
if len(parts) == 0 {
117-
return value, nil
118-
}
119-
120-
switch value.Kind() {
121-
case reflect.Ptr:
122-
return getTypeForFieldByName(value.Elem(), parts)
123-
124-
case reflect.Slice:
125-
return getTypeForFieldByName(value.Elem(), parts[1:])
126-
127-
case reflect.Map:
128-
return getTypeForFieldByName(value.Elem(), parts[1:])
129-
130-
case reflect.Struct:
131-
fieldName := strcase.ToPublicGoName(parts[0])
132-
field, hasField := value.FieldByName(fieldName)
133-
if !hasField {
134-
return nil, fmt.Errorf("field %v does not exist for %v", fieldName, value.Name())
135-
}
136-
return getTypeForFieldByName(field.Type, parts[1:])
137-
}
138-
139-
return nil, fmt.Errorf("type kind %s is not handled", value.Kind().String())
140-
}

internal/qa/argspec.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package qa
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/scaleway/scaleway-cli/v2/internal/args"
8+
"github.com/scaleway/scaleway-cli/v2/internal/core"
9+
)
10+
11+
type ArgSpecInvalidError struct {
12+
Command *core.Command
13+
argSpec *core.ArgSpec
14+
innerError error
15+
}
16+
17+
func (err ArgSpecInvalidError) Error() string {
18+
return fmt.Sprintf("command has invalid argspecs '%s' '%s' '%s'",
19+
err.Command.GetCommandLine("scw"),
20+
err.argSpec.Name,
21+
err.innerError,
22+
)
23+
}
24+
25+
// testArgSpecInvalidError tests that all argspecs have a corresponding in their command's argstype.
26+
func testArgSpecInvalidError(commands *core.Commands) []error {
27+
errors := []error(nil)
28+
29+
for _, command := range commands.GetAll() {
30+
for _, arg := range command.ArgSpecs {
31+
_, err := args.GetArgType(command.ArgsType, arg.Name)
32+
if err != nil {
33+
errors = append(errors, &ArgSpecInvalidError{Command: command, argSpec: arg, innerError: err})
34+
continue
35+
}
36+
}
37+
}
38+
39+
return errors
40+
}
41+
42+
type ArgSpecMissingError struct {
43+
Command *core.Command
44+
argName string
45+
}
46+
47+
func (err ArgSpecMissingError) Error() string {
48+
return fmt.Sprintf("command has a missing argspec '%s' '%s'",
49+
err.Command.GetCommandLine("scw"),
50+
err.argName,
51+
)
52+
}
53+
54+
// testArgSpecInvalidError tests that all argstype fields have a corresponding argspec.
55+
func testArgSpecMissingError(commands *core.Commands) []error {
56+
errors := []error(nil)
57+
58+
// Check all commands
59+
for _, command := range commands.GetAll() {
60+
if command.ArgsType == nil || command.ArgsType == reflect.TypeOf(args.RawArgs{}) {
61+
continue
62+
}
63+
64+
supposedArgSpecs := args.ListArgTypeFields(command.ArgsType)
65+
66+
for _, argSpecName := range supposedArgSpecs {
67+
if command.ArgSpecs.GetByName(argSpecName) == nil {
68+
errors = append(errors, &ArgSpecMissingError{Command: command, argName: argSpecName})
69+
}
70+
}
71+
}
72+
73+
return errors
74+
}

internal/qa/qa.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func LintCommands(commands *core.Commands) []error {
1919
errors = append(errors, testDifferentLocalizationForNamespaceError(commands)...)
2020
errors = append(errors, testDuplicatedCommandError(commands)...)
2121
errors = append(errors, testAtLeastOneExampleIsPresentError(commands)...)
22+
errors = append(errors, testArgSpecInvalidError(commands)...)
23+
errors = append(errors, testArgSpecMissingError(commands)...)
2224

2325
errors = filterIgnore(errors)
2426

0 commit comments

Comments
 (0)