github.com/coryb/figtree@v1.0.2-0.20230811213450-d30f28e27093/option.go (about) 1 package figtree 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "reflect" 7 "regexp" 8 9 "emperror.dev/errors" 10 "github.com/coryb/walky" 11 "gopkg.in/yaml.v3" 12 ) 13 14 const ( 15 defaultSource = "default" 16 overrideSource = "override" 17 promptSource = "prompt" 18 yamlSource = "yaml" 19 jsonSource = "json" 20 ) 21 22 type option interface { 23 IsDefined() bool 24 GetValue() any 25 SetValue(any) error 26 SetSource(SourceLocation) 27 GetSource() SourceLocation 28 IsDefault() bool 29 IsOverride() bool 30 } 31 32 // StringifyValue is global variable to indicate if the Option should be 33 // serialized as just the value (when value is true) or if the entire Option 34 // struct should be serialized. This is a hack, and not recommended for general 35 // usage, but can be useful for debugging. 36 var StringifyValue = true 37 38 // stringMapRegex is used in option parsing for map types Set routines 39 var stringMapRegex = regexp.MustCompile("[:=]") 40 41 // FileCoordinate represents the line/column of an option 42 type FileCoordinate struct { 43 Line int 44 Column int 45 } 46 47 // ideally these would be const if Go supported const structs? 48 var ( 49 // DefaultSource will be the value of the `Source` property 50 // for Option[T] when they are constructed via `NewOption[T]`. 51 DefaultSource = NewSource(defaultSource) 52 53 // OverrideSource will be the value of the `Source` property 54 // for Option[T] when they are populated via kingpin command 55 // line option. 56 OverrideSource = NewSource(overrideSource) 57 ) 58 59 type SourceLocation struct { 60 Name string 61 Location *FileCoordinate 62 } 63 64 func (s SourceLocation) String() string { 65 if s.Location != nil { 66 return fmt.Sprintf("%s:%d:%d", s.Name, s.Location.Line, s.Location.Column) 67 } 68 return s.Name 69 } 70 71 type SourceOption func(*SourceLocation) *SourceLocation 72 73 func WithLocation(location *FileCoordinate) SourceOption { 74 return func(s *SourceLocation) *SourceLocation { 75 s.Location = location 76 return s 77 } 78 } 79 80 func NewSource(name string, opts ...SourceOption) SourceLocation { 81 l := SourceLocation{ 82 Name: name, 83 } 84 for _, o := range opts { 85 o(&l) 86 } 87 return l 88 } 89 90 type Option[T any] struct { 91 Source SourceLocation 92 Defined bool 93 Value T 94 } 95 96 func NewOption[T any](dflt T) Option[T] { 97 return Option[T]{ 98 Source: NewSource(defaultSource), 99 Defined: true, 100 Value: dflt, 101 } 102 } 103 104 func (o Option[T]) IsDefined() bool { 105 return o.Defined 106 } 107 108 func (o *Option[T]) SetSource(source SourceLocation) { 109 o.Source = source 110 } 111 112 func (o *Option[T]) GetSource() SourceLocation { 113 return o.Source 114 } 115 116 func (o *Option[T]) IsDefault() bool { 117 return o.Source.Name == defaultSource 118 } 119 120 func (o *Option[T]) IsOverride() bool { 121 return o.Source.Name == overrideSource 122 } 123 124 func (o Option[T]) GetValue() any { 125 return o.Value 126 } 127 128 // WriteAnswer implements the Settable interface as defined by the 129 // survey prompting library: 130 // https://github.com/AlecAivazis/survey/blob/v2.3.5/core/write.go#L15-L18 131 func (o *Option[T]) WriteAnswer(name string, value any) error { 132 if v, ok := value.(T); ok { 133 o.Value = v 134 o.Defined = true 135 o.Source = NewSource(promptSource) 136 return nil 137 } 138 return errors.Errorf("Got %T expected %T type: %v", value, o.Value, value) 139 } 140 141 // Set implements part of the Value interface as defined by the kingpin command 142 // line option library: 143 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 144 func (o *Option[T]) Set(s string) error { 145 err := convertString(s, &o.Value) 146 if err != nil { 147 return err 148 } 149 o.Source = OverrideSource 150 o.Defined = true 151 return nil 152 } 153 154 // String implements part of the Value interface as defined by the kingpin 155 // command line option library: 156 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 157 func (o Option[T]) String() string { 158 if StringifyValue { 159 return fmt.Sprint(o.Value) 160 } 161 return fmt.Sprintf("{Source:%s Defined:%t Value:%v}", o.Source, o.Defined, o.Value) 162 } 163 164 // SetValue implements the Settings interface as defined by the kingpin 165 // command line option library: 166 // https://github.com/alecthomas/kingpin/blob/v1.3.4/parsers.go#L13-L15 167 func (o *Option[T]) SetValue(v any) error { 168 if val, ok := v.(T); ok { 169 o.Value = val 170 o.Defined = true 171 return nil 172 } 173 // look for type conversions as well, like: 174 // (*Option[float64]).SetValue(float32) 175 // There might be a better way to do this, but with 176 // Generics I could not find a better way to convert 177 // the input type to match the Option type. 178 dst := reflect.ValueOf(o.Value) 179 dstType := reflect.ValueOf(v).Type() 180 src := reflect.ValueOf(v) 181 if src.CanConvert(dstType) { 182 dst.Set(src.Convert(dstType)) 183 o.Defined = true 184 return nil 185 } 186 187 return errors.Errorf("Got %T expected %T type: %v", v, o.Value, v) 188 } 189 190 // UnmarshalYAML implement the Unmarshaler interface used by the 191 // yaml library: 192 // https://github.com/go-yaml/yaml/blob/v3.0.1/yaml.go#L36-L38 193 func (o *Option[T]) UnmarshalYAML(node *yaml.Node) error { 194 if err := node.Decode(&o.Value); err != nil { 195 return walky.NewYAMLError(err, node) 196 } 197 var loc *FileCoordinate 198 if node.Line > 0 || node.Column > 0 { 199 loc = &FileCoordinate{Line: node.Line, Column: node.Column} 200 } 201 o.Source = NewSource(yamlSource, WithLocation(loc)) 202 o.Defined = true 203 return nil 204 } 205 206 // MarshalYAML implements the Marshaler interface used by the yaml library: 207 // https://github.com/go-yaml/yaml/blob/v3.0.1/yaml.go#L50-L52 208 func (o Option[T]) MarshalYAML() (any, error) { 209 if StringifyValue { 210 // First double check if the Value has a custom Marshaler. 211 // Note we can't use `o.Value.(yaml.Marshaler)` directly because 212 // you cannot do type assertions on generic types. First we check 213 // if Value is a direct (non pointer) type 214 var q any = &o.Value 215 if marshaler, ok := q.(yaml.Marshaler); ok { 216 return marshaler.MarshalYAML() 217 } 218 // Now we try again for cases where Value is a pointer type. 219 q = o.Value 220 if marshaler, ok := q.(yaml.Marshaler); ok { 221 return marshaler.MarshalYAML() 222 } 223 return o.Value, nil 224 } 225 // need a copy of this struct without the MarshalYAML interface attached 226 return struct { 227 Value T 228 Source string 229 Defined bool 230 }{ 231 Value: o.Value, 232 Source: o.Source.String(), 233 Defined: o.Defined, 234 }, nil 235 } 236 237 // UnmarshalJSON implements the Unmarshaler interface as defined by json: 238 // https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/encoding/json/decode.go;l=118-120 239 func (o *Option[T]) UnmarshalJSON(b []byte) error { 240 if err := json.Unmarshal(b, &o.Value); err != nil { 241 return err 242 } 243 o.Source = NewSource(jsonSource) 244 o.Defined = true 245 return nil 246 } 247 248 // MarshalJSON implements the Marshaler interface as defined by json: 249 // https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/encoding/json/encode.go;l=225-227 250 func (o Option[T]) MarshalJSON() ([]byte, error) { 251 if StringifyValue { 252 return json.Marshal(o.Value) 253 } 254 // need a copy of this struct without the MarshalJSON interface attached 255 return json.Marshal(struct { 256 Value T 257 Source string 258 Defined bool 259 }{ 260 Value: o.Value, 261 Source: o.Source.String(), 262 Defined: o.Defined, 263 }) 264 } 265 266 // IsBoolFlag implements part of the boolFlag interface as defined by the 267 // kingpin command line option library: 268 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L42-L45 269 func (o Option[T]) IsBoolFlag() bool { 270 // TODO hopefully Go will get template specializations so we can 271 // implement this function specifically for Option[bool], but for 272 // now we have to use runtime reflection to determine the type. 273 v := reflect.ValueOf(o.Value) 274 if v.Kind() == reflect.Bool { 275 return true 276 } 277 return false 278 } 279 280 type MapOption[T any] map[string]Option[T] 281 282 // Set implements part of the Value interface as defined by the kingpin command 283 // line option library: 284 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 285 func (o *MapOption[T]) Set(value string) error { 286 parts := stringMapRegex.Split(value, 2) 287 if len(parts) != 2 { 288 return errors.Errorf("expected KEY=VALUE got '%s'", value) 289 } 290 val := Option[T]{} 291 if err := val.Set(parts[1]); err != nil { 292 return err 293 } 294 (*o)[parts[0]] = val 295 return nil 296 } 297 298 // IsCumulative implements part of the remainderArg interface as defined by the 299 // kingpin command line option library: 300 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L49-L52 301 func (o MapOption[T]) IsCumulative() bool { 302 return true 303 } 304 305 // String implements part of the Value interface as defined by the kingpin 306 // command line option library: 307 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 308 func (o MapOption[T]) String() string { 309 return fmt.Sprint(map[string]Option[T](o)) 310 } 311 312 func (o MapOption[T]) Map() map[string]T { 313 tmp := map[string]T{} 314 for k, v := range o { 315 tmp[k] = v.Value 316 } 317 return tmp 318 } 319 320 // WriteAnswer implements the Settable interface as defined by the 321 // survey prompting library: 322 // https://github.com/AlecAivazis/survey/blob/v2.3.5/core/write.go#L15-L18 323 func (o *MapOption[T]) WriteAnswer(name string, value any) error { 324 tmp := Option[T]{} 325 if v, ok := value.(T); ok { 326 tmp.Value = v 327 tmp.Defined = true 328 tmp.Source = NewSource(promptSource) 329 (*o)[name] = tmp 330 return nil 331 } 332 return errors.Errorf("Got %T expected %T type: %v", value, tmp.Value, value) 333 } 334 335 func (o MapOption[T]) IsDefined() bool { 336 // true if the map has any keys 337 return len(o) > 0 338 } 339 340 type ListOption[T any] []Option[T] 341 342 // Set implements part of the Value interface as defined by the kingpin command 343 // line option library: 344 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 345 func (o *ListOption[T]) Set(value string) error { 346 val := Option[T]{} 347 if err := val.Set(value); err != nil { 348 return err 349 } 350 *o = append(*o, val) 351 return nil 352 } 353 354 // WriteAnswer implements the Settable interface as defined by the 355 // survey prompting library: 356 // https://github.com/AlecAivazis/survey/blob/v2.3.5/core/write.go#L15-L18 357 func (o *ListOption[T]) WriteAnswer(name string, value any) error { 358 tmp := Option[T]{} 359 if v, ok := value.(T); ok { 360 tmp.Value = v 361 tmp.Defined = true 362 tmp.Source = NewSource(promptSource) 363 *o = append(*o, tmp) 364 return nil 365 } 366 return errors.Errorf("Got %T expected %T type: %v", value, tmp.Value, value) 367 } 368 369 // IsCumulative implements part of the remainderArg interface as defined by the 370 // kingpin command line option library: 371 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L49-L52 372 func (o ListOption[T]) IsCumulative() bool { 373 return true 374 } 375 376 // String implements part of the Value interface as defined by the kingpin 377 // command line option library: 378 // https://github.com/alecthomas/kingpin/blob/v1.3.4/values.go#L26-L29 379 func (o ListOption[T]) String() string { 380 return fmt.Sprint([]Option[T](o)) 381 } 382 383 func (o ListOption[T]) Append(values ...T) ListOption[T] { 384 results := o 385 for _, val := range values { 386 results = append(results, NewOption(val)) 387 } 388 return results 389 } 390 391 func (o ListOption[T]) Slice() []T { 392 tmp := []T{} 393 for _, elem := range o { 394 tmp = append(tmp, elem.Value) 395 } 396 return tmp 397 } 398 399 func (o ListOption[T]) IsDefined() bool { 400 // true if the list is not empty 401 return len(o) > 0 402 }