Skip to content

Commit 3ec0e1a

Browse files
authored
fix(core): allow required nil values (#881)
* fix(core): allow required nil values Signed-off-by: Patrik Cyvoct <[email protected]>
1 parent 5d3c3ad commit 3ec0e1a

File tree

4 files changed

+77
-11
lines changed

4 files changed

+77
-11
lines changed

internal/args/args.go

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ const emptySliceValue = "none"
1515
// RawArgs allows to retrieve a simple []string using UnmarshalStruct()
1616
type RawArgs []string
1717

18+
// ExistsArgByName checks if the given argument exists in the raw args
19+
func (a RawArgs) ExistsArgByName(name string) bool {
20+
argsMap := SplitRawMap(a)
21+
_, ok := argsMap[name]
22+
return ok
23+
}
24+
1825
var (
1926
scalarKinds = map[reflect.Kind]bool{
2027
reflect.Int: true,

internal/core/cobra_utils.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func cobraRun(ctx context.Context, cmd *Command) func(*cobra.Command, []string)
7373
if cmd.ValidateFunc != nil {
7474
validateFunc = cmd.ValidateFunc
7575
}
76-
err = validateFunc(cmd, cmdArgs)
76+
err = validateFunc(cmd, cmdArgs, rawArgs)
7777
if err != nil {
7878
return err
7979
}

internal/core/validate.go

+17-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package core
33
import (
44
"fmt"
55
"reflect"
6+
"strconv"
67
"strings"
78

89
"github.com/scaleway/scaleway-cli/internal/args"
@@ -13,19 +14,19 @@ import (
1314

1415
// CommandValidateFunc validates en entire command.
1516
// Used in core.cobraRun().
16-
type CommandValidateFunc func(cmd *Command, cmdArgs interface{}) error
17+
type CommandValidateFunc func(cmd *Command, cmdArgs interface{}, rawArgs args.RawArgs) error
1718

1819
// ArgSpecValidateFunc validates one argument of a command.
1920
type ArgSpecValidateFunc func(argSpec *ArgSpec, value interface{}) error
2021

2122
// DefaultCommandValidateFunc is the default validation function for commands.
2223
func DefaultCommandValidateFunc() CommandValidateFunc {
23-
return func(cmd *Command, cmdArgs interface{}) error {
24+
return func(cmd *Command, cmdArgs interface{}, rawArgs args.RawArgs) error {
2425
err := validateArgValues(cmd, cmdArgs)
2526
if err != nil {
2627
return err
2728
}
28-
err = validateRequiredArgs(cmd, cmdArgs)
29+
err = validateRequiredArgs(cmd, cmdArgs, rawArgs)
2930
if err != nil {
3031
return err
3132
}
@@ -59,8 +60,13 @@ func validateArgValues(cmd *Command, cmdArgs interface{}) error {
5960
// validateRequiredArgs checks for missing required args with no default value.
6061
// Returns an error for the first missing required arg.
6162
// Returns nil otherwise.
62-
func validateRequiredArgs(cmd *Command, cmdArgs interface{}) error {
63+
// TODO refactor this method which uses a mix of reflect and string arrays
64+
func validateRequiredArgs(cmd *Command, cmdArgs interface{}, rawArgs args.RawArgs) error {
6365
for _, arg := range cmd.ArgSpecs {
66+
if !arg.Required {
67+
continue
68+
}
69+
6470
fieldName := strcase.ToPublicGoName(arg.Name)
6571
fieldValues, err := getValuesForFieldByName(reflect.ValueOf(cmdArgs), strings.Split(fieldName, "."))
6672
if err != nil {
@@ -72,9 +78,13 @@ func validateRequiredArgs(cmd *Command, cmdArgs interface{}) error {
7278
panic(validationErr)
7379
}
7480

75-
for _, fieldValue := range fieldValues {
76-
if arg.Required && (fieldValue.IsZero() || !fieldValue.IsValid()) {
77-
return MissingRequiredArgumentError(arg.Name)
81+
// Either fieldsValues have a length for 1 and we check for existence in the rawArgs
82+
// or it has multiple values and we loop through each one to get the right element in
83+
// the corresponding rawArgs array and replace {index} by the element's index.
84+
// TODO handle required maps
85+
for i := range fieldValues {
86+
if !rawArgs.ExistsArgByName(strings.Replace(arg.Name, "{index}", strconv.Itoa(i), 1)) {
87+
return MissingRequiredArgumentError(strings.Replace(arg.Name, "{index}", strconv.Itoa(i), 1))
7888
}
7989
}
8090
}

internal/core/validate_test.go

+52-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/alecthomas/assert"
8+
"github.com/scaleway/scaleway-cli/internal/args"
89
)
910

1011
type Element struct {
@@ -31,11 +32,12 @@ func Test_DefaultCommandValidateFunc(t *testing.T) {
3132
type TestCase struct {
3233
command *Command
3334
parsedArguments interface{}
35+
rawArgs args.RawArgs
3436
}
3537

3638
run := func(testCase TestCase) func(t *testing.T) {
3739
return func(t *testing.T) {
38-
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments)
40+
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments, testCase.rawArgs)
3941
assert.Equal(t, fmt.Errorf("arg validation called"), err)
4042
}
4143
}
@@ -187,18 +189,19 @@ func Test_DefaultCommandRequiredFunc(t *testing.T) {
187189
type TestCase struct {
188190
command *Command
189191
parsedArguments interface{}
192+
rawArgs args.RawArgs
190193
}
191194

192195
runOK := func(testCase TestCase) func(t *testing.T) {
193196
return func(t *testing.T) {
194-
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments)
197+
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments, testCase.rawArgs)
195198
assert.Equal(t, nil, err)
196199
}
197200
}
198201

199202
runErr := func(testCase TestCase, argName string) func(t *testing.T) {
200203
return func(t *testing.T) {
201-
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments)
204+
err := DefaultCommandValidateFunc()(testCase.command, testCase.parsedArguments, testCase.rawArgs)
202205
assert.Equal(t, MissingRequiredArgumentError(argName), err)
203206
}
204207
}
@@ -212,6 +215,7 @@ func Test_DefaultCommandRequiredFunc(t *testing.T) {
212215
},
213216
},
214217
},
218+
rawArgs: []string{"first-nested-element.second-nested-element=test"},
215219
parsedArguments: &elementCustom{
216220
Element: &Element{
217221
Name: "nested",
@@ -238,4 +242,49 @@ func Test_DefaultCommandRequiredFunc(t *testing.T) {
238242
},
239243
},
240244
}, "first-nested-element.second-nested-element"))
245+
246+
t.Run("required-index", runOK(TestCase{
247+
command: &Command{
248+
ArgSpecs: ArgSpecs{
249+
{
250+
Name: "elements-slice.{index}.id",
251+
Required: true,
252+
},
253+
},
254+
},
255+
rawArgs: []string{"elements-slice.0.id=1"},
256+
parsedArguments: &Element{
257+
ElementsSlice: []Element{
258+
{
259+
ID: 0,
260+
Name: "1",
261+
},
262+
},
263+
},
264+
}))
265+
266+
t.Run("fail-required-index", runErr(TestCase{
267+
command: &Command{
268+
ArgSpecs: ArgSpecs{
269+
{
270+
Name: "elements-slice.{index}.id",
271+
Required: true,
272+
},
273+
},
274+
},
275+
rawArgs: []string{"elements-slice.0.id=1"},
276+
parsedArguments: &Element{
277+
ElementsSlice: []Element{
278+
{
279+
ID: 0,
280+
Name: "1",
281+
},
282+
{
283+
ID: 1,
284+
Name: "0",
285+
},
286+
},
287+
},
288+
}, "elements-slice.1.id"))
289+
241290
}

0 commit comments

Comments
 (0)