github.com/octohelm/wagon@v0.0.0-20240308040401-88662650dc0b/pkg/engine/plan/internal/cueify.go (about)

     1  package internal
     2  
     3  import (
     4  	"bytes"
     5  	"encoding"
     6  	"fmt"
     7  	"go/ast"
     8  	"reflect"
     9  	"strings"
    10  )
    11  
    12  type OneOfType interface {
    13  	OneOf() []any
    14  }
    15  
    16  var oneOfType = reflect.TypeOf((*OneOfType)(nil)).Elem()
    17  var textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
    18  
    19  func newConvert(r TaskRegister) *convert {
    20  	return &convert{
    21  		r:    r,
    22  		defs: map[reflect.Type]bool{},
    23  	}
    24  }
    25  
    26  type convert struct {
    27  	r    TaskRegister
    28  	defs map[reflect.Type]bool
    29  }
    30  
    31  type opt struct {
    32  	naming string
    33  	extra  string
    34  }
    35  
    36  func (c *convert) toCueType(tpe reflect.Type, o opt) []byte {
    37  	if o.naming == "" && tpe.PkgPath() != "" {
    38  		if _, ok := c.defs[tpe]; !ok {
    39  			c.defs[tpe] = true
    40  			c.r.Register(reflect.New(tpe).Interface())
    41  		}
    42  
    43  		if o.extra != "" {
    44  			return []byte(fmt.Sprintf(`#%s & { 
    45    %s 
    46  }`, tpe.Name(), o.extra))
    47  		}
    48  		return []byte(fmt.Sprintf("#%s", tpe.Name()))
    49  	}
    50  
    51  	if tpe.Implements(textMarshalerType) {
    52  		return []byte("string")
    53  	}
    54  
    55  	if tpe.Implements(oneOfType) {
    56  		if ot, ok := reflect.New(tpe).Interface().(OneOfType); ok {
    57  			types := ot.OneOf()
    58  			b := bytes.NewBuffer(nil)
    59  
    60  			for i := range types {
    61  				t := reflect.TypeOf(types[i])
    62  				if t.Kind() == reflect.Ptr {
    63  					t = t.Elem()
    64  				}
    65  				if i > 0 {
    66  					b.WriteString(" | ")
    67  				}
    68  				b.Write(c.toCueType(t, opt{extra: o.extra}))
    69  			}
    70  
    71  			return b.Bytes()
    72  		}
    73  	}
    74  
    75  	switch tpe.Kind() {
    76  	case reflect.Ptr:
    77  		return []byte(fmt.Sprintf("%s | null", c.toCueType(tpe.Elem(), opt{extra: o.extra})))
    78  	case reflect.Map:
    79  		return []byte(fmt.Sprintf("[X=%s]: %s", c.toCueType(tpe.Key(), opt{extra: o.extra}), c.toCueType(tpe.Elem(), opt{extra: o.extra})))
    80  	case reflect.Slice:
    81  		return []byte(fmt.Sprintf("[...%s]", c.toCueType(tpe.Elem(), opt{extra: o.extra})))
    82  	case reflect.Struct:
    83  		b := bytes.NewBuffer(nil)
    84  
    85  		_, _ = fmt.Fprintf(b, `{
    86  `)
    87  
    88  		walkFields(tpe, func(i *fieldInfo) {
    89  			t := i.tpe
    90  
    91  			// FIXME may support other inline
    92  			if i.inline {
    93  				if t.Kind() == reflect.Map {
    94  					_, _ = fmt.Fprintf(b, `[!~"\\$wagon"]: %s`, c.toCueType(t.Elem(), opt{
    95  						extra: i.cueExtra,
    96  					}))
    97  				}
    98  				return
    99  			}
   100  
   101  			if i.optional {
   102  				if t.Kind() == reflect.Ptr {
   103  					t = t.Elem()
   104  				}
   105  				_, _ = fmt.Fprintf(b, "%s?: ", i.name)
   106  			} else {
   107  				_, _ = fmt.Fprintf(b, "%s: ", i.name)
   108  			}
   109  
   110  			cueType := c.toCueType(t, opt{
   111  				extra: i.cueExtra,
   112  			})
   113  
   114  			if len(i.enum) > 0 {
   115  				for i, e := range i.enum {
   116  					if i > 0 {
   117  						_, _ = fmt.Fprint(b, " | ")
   118  					}
   119  					_, _ = fmt.Fprintf(b, `%q`, e)
   120  				}
   121  			} else {
   122  				_, _ = fmt.Fprintf(b, "%s", cueType)
   123  			}
   124  
   125  			if i.defaultValue != nil {
   126  				switch string(cueType) {
   127  				case "[]byte":
   128  					_, _ = fmt.Fprintf(b, ` | *'%s'`, *i.defaultValue)
   129  				case "string":
   130  					_, _ = fmt.Fprintf(b, ` | *%q`, *i.defaultValue)
   131  				default:
   132  					_, _ = fmt.Fprintf(b, ` | *%v`, *i.defaultValue)
   133  				}
   134  			}
   135  
   136  			if len(i.attrs) > 0 {
   137  				_, _ = fmt.Fprintf(b, " @wagon(%s)", strings.Join(i.attrs, ","))
   138  			}
   139  
   140  			_, _ = fmt.Fprint(b, "\n")
   141  		})
   142  
   143  		_, _ = fmt.Fprintf(b, `}`)
   144  
   145  		return b.Bytes()
   146  	case reflect.Interface:
   147  		return []byte("_")
   148  	default:
   149  		return []byte(tpe.Kind().String())
   150  	}
   151  }
   152  
   153  type fieldInfo struct {
   154  	name         string
   155  	cueExtra     string
   156  	idx          int
   157  	tpe          reflect.Type
   158  	optional     bool
   159  	inline       bool
   160  	defaultValue *string
   161  	enum         []string
   162  	attrs        []string
   163  }
   164  
   165  func (i *fieldInfo) EmptyDefaults() (string, bool) {
   166  	if i.tpe.PkgPath() != "" {
   167  		return "", false
   168  	}
   169  
   170  	switch i.tpe.Kind() {
   171  	case reflect.Slice:
   172  		return "", false
   173  	case reflect.Map:
   174  		return "", false
   175  	case reflect.Interface:
   176  		return "", false
   177  	}
   178  	return fmt.Sprintf("%v", reflect.New(i.tpe).Elem()), true
   179  }
   180  
   181  func (i *fieldInfo) HasAttr(expectAttr string) bool {
   182  	for _, attr := range i.attrs {
   183  		if attr == expectAttr {
   184  			return true
   185  		}
   186  	}
   187  	return false
   188  }
   189  
   190  func walkFields(s reflect.Type, each func(info *fieldInfo)) {
   191  	for i := 0; i < s.NumField(); i++ {
   192  		f := s.Field(i)
   193  
   194  		if !ast.IsExported(f.Name) {
   195  			continue
   196  		}
   197  
   198  		info := &fieldInfo{}
   199  		info.idx = i
   200  		info.name = f.Name
   201  		info.tpe = f.Type
   202  
   203  		jsonTag, hasJsonTag := f.Tag.Lookup("json")
   204  		if !hasJsonTag {
   205  			if f.Anonymous && f.Type.Kind() == reflect.Struct {
   206  				walkFields(f.Type, each)
   207  			}
   208  			continue
   209  		}
   210  
   211  		if strings.Contains(jsonTag, ",omitempty") {
   212  			info.optional = true
   213  		}
   214  
   215  		if strings.Contains(jsonTag, ",inline") {
   216  			info.inline = true
   217  			info.name = ""
   218  		}
   219  
   220  		if cueExtra, hasCueExtra := f.Tag.Lookup("cueExtra"); hasCueExtra {
   221  			info.cueExtra = cueExtra
   222  		}
   223  
   224  		wagonTag, hasWagonTag := f.Tag.Lookup("wagon")
   225  		if jsonTag == "-" && !hasWagonTag {
   226  			continue
   227  		}
   228  
   229  		if jsonName := strings.SplitN(jsonTag, ",", 2)[0]; jsonName != "" {
   230  			info.name = jsonName
   231  		}
   232  
   233  		if hasWagonTag {
   234  			attrs := strings.Split(wagonTag, ",")
   235  
   236  			for _, n := range attrs {
   237  				parts := strings.SplitN(n, "=", 2)
   238  				if len(parts) == 2 && parts[0] == "name" {
   239  					info.name = parts[1]
   240  					continue
   241  				}
   242  				info.attrs = append(info.attrs, n)
   243  			}
   244  		}
   245  
   246  		if defaultValue, ok := f.Tag.Lookup("default"); ok {
   247  			info.defaultValue = &defaultValue
   248  		}
   249  
   250  		if enumValue, ok := f.Tag.Lookup("enum"); ok {
   251  			info.enum = strings.Split(enumValue, ",")
   252  		}
   253  
   254  		each(info)
   255  	}
   256  }