Skip to content

Commit af1e600

Browse files
jremy42scaleway-botremyleone
authored
feat(core):handle oneof validation (#3727)
Co-authored-by: Scaleway Bot <[email protected]> Co-authored-by: Rémy Léone <[email protected]>
1 parent f123277 commit af1e600

File tree

5 files changed

+752
-22
lines changed

5 files changed

+752
-22
lines changed

cmd/scw/testdata/test-all-usage-rdb-instance-create-usage.golden

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,23 @@ USAGE:
66
scw rdb instance create [arg=value ...]
77

88
ARGS:
9-
[project-id] Project ID to use. If none is passed the default project ID will be used
10-
[name=<generated>] Name of the Database Instance
11-
engine Database engine of the Database Instance (PostgreSQL, MySQL, ...)
12-
user-name Username created when the Database Instance is created
13-
[generate-password=true] Will generate a 21 character-length password that contains a mix of upper/lower case letters, numbers and special symbols
14-
[password] Password of the user
15-
node-type=DB-DEV-S Type of node to use for the Database Instance
16-
[is-ha-cluster] Defines whether or not High-Availability is enabled
17-
[disable-backup] Defines whether or not backups are disabled
18-
[tags.{index}] Tags to apply to the Database Instance
19-
[init-settings.{index}.name]
20-
[init-settings.{index}.value]
21-
[volume-type] Type of volume where data is stored (lssd, bssd, ...) (lssd | bssd | sbs_5k | sbs_15k)
22-
[volume-size] Volume size when volume_type is not lssd
23-
[init-endpoints.{index}.load-balancer=false] Will configure a load-balancer endpoint along with your private network endpoint if true
24-
[init-endpoints.{index}.private-network.enable-ipam=false] Will configure your Private Network endpoint with Scaleway IPAM service if true
9+
[project-id] Project ID to use. If none is passed the default project ID will be used
10+
[name=<generated>] Name of the Database Instance
11+
engine Database engine of the Database Instance (PostgreSQL, MySQL, ...)
12+
user-name Username created when the Database Instance is created
13+
[generate-password=true] Will generate a 21 character-length password that contains a mix of upper/lower case letters, numbers and special symbols
14+
[password] Password of the user
15+
node-type=DB-DEV-S Type of node to use for the Database Instance
16+
[is-ha-cluster] Defines whether or not High-Availability is enabled
17+
[disable-backup] Defines whether or not backups are disabled
18+
[tags.{index}] Tags to apply to the Database Instance
19+
[init-settings.{index}.name]
20+
[init-settings.{index}.value]
21+
[volume-type] Type of volume where data is stored (lssd, bssd, ...) (lssd | bssd | sbs_5k | sbs_15k)
22+
[volume-size] Volume size when volume_type is not lssd
23+
[init-endpoints.{index}.load-balancer=false] Will configure a load-balancer endpoint along with your private network endpoint if true
24+
config (one of):
25+
[init-endpoints.{index}.private-network.enable-ipam] Will configure your Private Network endpoint with Scaleway IPAM service if true
2526
[init-endpoints.{index}.private-network.private-network-id] UUID of the Private Network to be connected to the Database Instance
2627
[init-endpoints.{index}.private-network.service-ip] Endpoint IPv4 address with a CIDR notation. Refer to the official Scaleway documentation to learn more about IP and subnet limitations.
2728
[backup-same-region] Defines whether to or not to store logical backups in the same region as the Database Instance

docs/commands/rdb.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ scw rdb instance create [arg=value ...]
762762
| volume-type | One of: `lssd`, `bssd`, `sbs_5k`, `sbs_15k` | Type of volume where data is stored (lssd, bssd, ...) |
763763
| volume-size | | Volume size when volume_type is not lssd |
764764
| init-endpoints.{index}.load-balancer | Default: `false` | Will configure a load-balancer endpoint along with your private network endpoint if true |
765-
| init-endpoints.{index}.private-network.enable-ipam | Default: `false` | Will configure your Private Network endpoint with Scaleway IPAM service if true |
765+
| init-endpoints.{index}.private-network.enable-ipam | | Will configure your Private Network endpoint with Scaleway IPAM service if true |
766766
| init-endpoints.{index}.private-network.private-network-id | | UUID of the Private Network to be connected to the Database Instance |
767767
| init-endpoints.{index}.private-network.service-ip | | Endpoint IPv4 address with a CIDR notation. Refer to the official Scaleway documentation to learn more about IP and subnet limitations. |
768768
| backup-same-region | | Defines whether to or not to store logical backups in the same region as the Database Instance |

internal/core/validate.go

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type CommandValidateFunc func(ctx context.Context, cmd *Command, cmdArgs interfa
2020
// ArgSpecValidateFunc validates one argument of a command.
2121
type ArgSpecValidateFunc func(argSpec *ArgSpec, value interface{}) error
2222

23+
type OneOfGroupManager struct {
24+
Groups map[string][]string
25+
RequiredGroups map[string]bool
26+
}
27+
2328
// DefaultCommandValidateFunc is the default validation function for commands.
2429
func DefaultCommandValidateFunc() CommandValidateFunc {
2530
return func(ctx context.Context, cmd *Command, cmdArgs interface{}, rawArgs args.RawArgs) error {
@@ -70,7 +75,7 @@ func validateArgValues(cmd *Command, cmdArgs interface{}) error {
7075
// TODO refactor this method which uses a mix of reflect and string arrays
7176
func validateRequiredArgs(cmd *Command, cmdArgs interface{}, rawArgs args.RawArgs) error {
7277
for _, arg := range cmd.ArgSpecs {
73-
if !arg.Required {
78+
if !arg.Required || arg.OneOfGroup != "" {
7479
continue
7580
}
7681

@@ -95,6 +100,21 @@ func validateRequiredArgs(cmd *Command, cmdArgs interface{}, rawArgs args.RawArg
95100
}
96101
}
97102
}
103+
if err := validateOneOfRequiredArgs(cmd, rawArgs, cmdArgs); err != nil {
104+
return err
105+
}
106+
107+
return nil
108+
}
109+
110+
func validateOneOfRequiredArgs(cmd *Command, rawArgs args.RawArgs, cmdArgs interface{}) error {
111+
oneOfManager := NewOneOfGroupManager(cmd)
112+
if err := oneOfManager.ValidateUniqueOneOfGroups(rawArgs, cmdArgs); err != nil {
113+
return err
114+
}
115+
if err := oneOfManager.ValidateRequiredOneOfGroups(rawArgs, cmdArgs); err != nil {
116+
return err
117+
}
98118
return nil
99119
}
100120

@@ -249,3 +269,78 @@ func ValidateProjectID() ArgSpecValidateFunc {
249269
return nil
250270
}
251271
}
272+
273+
func NewOneOfGroupManager(cmd *Command) *OneOfGroupManager {
274+
manager := &OneOfGroupManager{
275+
Groups: make(map[string][]string),
276+
RequiredGroups: make(map[string]bool),
277+
}
278+
279+
for _, arg := range cmd.ArgSpecs {
280+
if arg.OneOfGroup != "" {
281+
manager.Groups[arg.OneOfGroup] = append(manager.Groups[arg.OneOfGroup], arg.Name)
282+
if arg.Required {
283+
manager.RequiredGroups[arg.OneOfGroup] = true
284+
}
285+
}
286+
}
287+
288+
return manager
289+
}
290+
291+
func (m *OneOfGroupManager) ValidateUniqueOneOfGroups(rawArgs args.RawArgs, cmdArgs interface{}) error {
292+
for groupName, groupArgs := range m.Groups {
293+
existingArg := ""
294+
for _, argName := range groupArgs {
295+
fieldName := strcase.ToPublicGoName(argName)
296+
fieldValues, err := GetValuesForFieldByName(reflect.ValueOf(cmdArgs), strings.Split(fieldName, "."))
297+
if err != nil {
298+
validationErr := fmt.Errorf("could not validate arg value for '%v': invalid field name '%v': %v", argName, fieldName, err.Error())
299+
if m.RequiredGroups[groupName] {
300+
logger.Infof(validationErr.Error())
301+
continue
302+
}
303+
panic(validationErr)
304+
}
305+
for i := range fieldValues {
306+
argNameWithIndex := strings.Replace(argName, "{index}", strconv.Itoa(i), 1)
307+
if rawArgs.ExistsArgByName(argNameWithIndex) {
308+
if existingArg != "" {
309+
return fmt.Errorf("arguments '%s' and '%s' are mutually exclusive", existingArg, argNameWithIndex)
310+
}
311+
existingArg = argNameWithIndex
312+
}
313+
}
314+
}
315+
}
316+
return nil
317+
}
318+
319+
func (m *OneOfGroupManager) ValidateRequiredOneOfGroups(rawArgs args.RawArgs, cmdArgs interface{}) error {
320+
for group, required := range m.RequiredGroups {
321+
if required {
322+
found := false
323+
for _, argName := range m.Groups[group] {
324+
fieldName := strcase.ToPublicGoName(argName)
325+
fieldValues, err := GetValuesForFieldByName(reflect.ValueOf(cmdArgs), strings.Split(fieldName, "."))
326+
if err != nil {
327+
validationErr := fmt.Errorf("could not validate arg value for '%v': invalid field name '%v': %v", argName, fieldName, err.Error())
328+
panic(validationErr)
329+
}
330+
for i := range fieldValues {
331+
if rawArgs.ExistsArgByName(strings.Replace(argName, "{index}", strconv.Itoa(i), 1)) {
332+
found = true
333+
break
334+
}
335+
}
336+
if found {
337+
break
338+
}
339+
}
340+
if !found {
341+
return fmt.Errorf("at least one argument from the '%s' group is required", group)
342+
}
343+
}
344+
}
345+
return nil
346+
}

0 commit comments

Comments
 (0)