github.com/facebookincubator/ttpforge@v1.0.13-0.20240405153150-5ae801628835/pkg/args/spec.go (about) 1 /* 2 Copyright © 2023-present, Meta Platforms, Inc. and affiliates 3 Permission is hereby granted, free of charge, to any person obtaining a copy 4 of this software and associated documentation files (the "Software"), to deal 5 in the Software without restriction, including without limitation the rights 6 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 copies of the Software, and to permit persons to whom the Software is 8 furnished to do so, subject to the following conditions: 9 The above copyright notice and this permission notice shall be included in 10 all copies or substantial portions of the Software. 11 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 THE SOFTWARE. 18 */ 19 20 package args 21 22 import ( 23 "errors" 24 "fmt" 25 "regexp" 26 "strconv" 27 "strings" 28 29 "github.com/facebookincubator/ttpforge/pkg/fileutils" 30 ) 31 32 // Spec defines a CLI argument for the TTP 33 type Spec struct { 34 Name string `yaml:"name"` 35 Type string `yaml:"type,omitempty"` 36 Default string `yaml:"default,omitempty"` 37 Choices []string `yaml:"choices,omitempty"` 38 Format string `yaml:"regexp,omitempty"` 39 40 formatReg *regexp.Regexp 41 } 42 43 // ParseAndValidate checks that the provided arguments 44 // match the argument specifications for this TTP 45 // 46 // **Parameters:** 47 // 48 // specs: slice of argument Spec values loaded from the TTP yaml 49 // argKvStrs: slice of arguments in "ARG_NAME=ARG_VALUE" format 50 // 51 // **Returns:** 52 // 53 // map[string]string: the parsed and validated argument key-value pairs 54 // error: an error if there is a problem 55 func ParseAndValidate(specs []Spec, argsKvStrs []string) (map[string]any, error) { 56 57 // validate the specs 58 processedArgs := make(map[string]any) 59 specsByName := make(map[string]Spec) 60 for _, spec := range specs { 61 if spec.Name == "" { 62 return nil, errors.New("argument name cannot be empty") 63 } 64 65 err := spec.validateChoiceTypes() 66 if err != nil { 67 return nil, fmt.Errorf("failed to validate types of choice values: %w", err) 68 } 69 70 // set the default value, will be overwritten by passed value 71 if spec.Default != "" { 72 if !spec.isValidChoice(spec.Default) { 73 return nil, fmt.Errorf("invalid default value: %v, allowed values: %v ", spec.Default, strings.Join(spec.Choices, ", ")) 74 } 75 76 defaultVal, err := spec.convertArgToType(spec.Default) 77 if err != nil { 78 return nil, fmt.Errorf("default value type does not match spec: %w", err) 79 } 80 processedArgs[spec.Name] = defaultVal 81 } 82 83 // set Format to match whole string 84 // check if first and last character are ^ and $ respectively 85 // append and prepend if missing 86 // if Format string is missing ^$ then we are subject to partial matches 87 if spec.Format != "" { 88 if err := verifyCanUseWithRegexp(spec); err != nil { 89 return nil, err 90 } 91 spec.formatReg, err = regexp.Compile(spec.Format) 92 if err != nil { 93 return nil, fmt.Errorf("invalid regular expression supplied to arg spec format: %w", err) 94 } 95 } 96 97 if _, ok := specsByName[spec.Name]; ok { 98 return nil, fmt.Errorf("duplicate argument name: %v", spec.Name) 99 } 100 specsByName[spec.Name] = spec 101 } 102 103 // validate the inputs 104 for _, argKvStr := range argsKvStrs { 105 argKv := strings.SplitN(argKvStr, "=", 2) 106 if len(argKv) != 2 { 107 return nil, fmt.Errorf("invalid argument specification string: %v", argKvStr) 108 } 109 argName := argKv[0] 110 argVal := argKv[1] 111 112 // passed foo=bar with no argument foo defined in specs 113 spec, ok := specsByName[argName] 114 if !ok { 115 return nil, fmt.Errorf("received unexpected argument: %v ", argName) 116 } 117 118 if !spec.isValidChoice(argVal) { 119 return nil, fmt.Errorf("received unexpected value: %v, allowed values: %v ", argVal, strings.Join(spec.Choices, ", ")) 120 } 121 122 if spec.formatReg != nil && !spec.formatReg.MatchString(argVal) { 123 return nil, fmt.Errorf("invalid value format: %v, expected regex format: %v ", argVal, spec.Format) 124 } 125 126 typedVal, err := spec.convertArgToType(argVal) 127 if err != nil { 128 return nil, fmt.Errorf( 129 "failed to process value '%v' specified for argument '%v': %v", 130 argVal, 131 argName, 132 err, 133 ) 134 } 135 136 // valid arg value - save 137 processedArgs[argName] = typedVal 138 } 139 140 // error if argument was not provided and no default value was specified 141 for _, spec := range specs { 142 if _, ok := processedArgs[spec.Name]; !ok { 143 return nil, fmt.Errorf("value for required argument '%v' was not provided and no default value was specified", spec.Name) 144 } 145 } 146 return processedArgs, nil 147 } 148 149 func (spec Spec) convertArgToType(val string) (any, error) { 150 switch spec.Type { 151 case "", "string": 152 // string is the default - any string is valid 153 return val, nil 154 case "int": 155 asInt, err := strconv.Atoi(val) 156 if err != nil { 157 return nil, errors.New("non-integer value provided") 158 } 159 return asInt, nil 160 case "bool": 161 asBool, err := strconv.ParseBool(val) 162 if err != nil { 163 return nil, errors.New("no-boolean value provided") 164 } 165 return asBool, nil 166 case "path": 167 absPath, err := fileutils.AbsPath(val) 168 if err != nil { 169 return nil, fmt.Errorf("failed to process argument of type `path`: %w", err) 170 } 171 return absPath, nil 172 default: 173 return nil, fmt.Errorf("invalid type %v specified in configuration for argument %v", spec.Type, spec.Name) 174 } 175 }