github.com/vishnusomank/figtree@v0.1.0/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  }