github.com/solo-io/cue@v0.4.7/encoding/gocode/generator.go (about) 1 // Copyright 2019 CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gocode 16 17 import ( 18 "bytes" 19 "fmt" 20 "go/ast" 21 "go/format" 22 "go/types" 23 "text/template" 24 25 "golang.org/x/tools/go/packages" 26 27 "github.com/solo-io/cue/cue" 28 "github.com/solo-io/cue/cue/errors" 29 "github.com/solo-io/cue/internal/value" 30 ) 31 32 // Config defines options for generation Go code. 33 type Config struct { 34 // Prefix is used as a prefix to all generated variables. It defaults to 35 // cuegen. 36 Prefix string 37 38 // ValidateName defines the default name for validation methods or prefix 39 // for validation functions. The default is "Validate". Set to "-" to 40 // disable generating validators. 41 ValidateName string 42 43 // CompleteName defines the default name for complete methods or prefix 44 // for complete functions. The default is "-" (disabled). 45 CompleteName string 46 47 // The cue.Runtime variable name to use for initializing Codecs. 48 // A new Runtime is created by default. 49 RuntimeVar string 50 } 51 52 const defaultPrefix = "cuegen" 53 54 // Generate generates Go code for the given instance in the directory of the 55 // given package. 56 // 57 // Generate converts top-level declarations to corresponding Go code. By default, 58 // it will only generate validation functions of methods for exported top-level 59 // declarations. The behavior can be altered with the @go attribute. 60 // 61 // The go attribute has the following form @go(<name>{,<option>}), where option 62 // is either a key-value pair or a flag. The name maps the CUE name to an 63 // alternative Go name. The special value '-' is used to indicate the field 64 // should be ignored for any Go generation. 65 // 66 // The following options are supported: 67 // 68 // type=<gotype> The Go type as which this value should be interpreted. 69 // This defaults to the type with the (possibly overridden) 70 // name of the field. 71 // validate=<name> Alternative name for the validation function or method 72 // Setting this to the empty string disables generation. 73 // complete=<name> Alternative name for the validation function or method. 74 // Setting this to the empty string disables generation. 75 // func Generate as a function instead of a method. 76 // 77 // 78 // Selection and Naming 79 // 80 // Generate will not generate any code for fields that have no go attribute 81 // and that are not exported or for which there is no namesake Go type. 82 // If the go attribute has the special value '-' as its name it wil be dropped 83 // as well. In all other cases Generate will generate Go code, even if the 84 // resulting code will not compile. For instance, Generate will generate Go 85 // code even if the user defines a Go type in the attribute that does not 86 // exist. 87 // 88 // If a field selected for generation and the go name matches that the name of 89 // the Go type, the corresponding validate and complete code are generated as 90 // methods by default. If not, it will be generated as a function. The default 91 // function name is the default operation name with the Go name as a suffix. 92 // 93 // 94 // Caveats 95 // Currently not supported: 96 // - option to generate Go structs (or automatically generate if undefined) 97 // - for type option to refer to types outside the package. 98 // 99 func Generate(pkgPath string, inst *cue.Instance, c *Config) (b []byte, err error) { 100 // TODO: if inst is nil, the instance is loaded from CUE files in the same 101 // package directory with the same package name. 102 if err = inst.Value().Validate(); err != nil { 103 return nil, err 104 } 105 if c == nil { 106 c = &Config{} 107 } 108 g := &generator{ 109 Config: *c, 110 111 typeMap: map[string]types.Type{}, 112 } 113 114 pkgName := inst.PkgName 115 116 if pkgPath != "" { 117 loadCfg := &packages.Config{ 118 Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps, 119 } 120 pkgs, err := packages.Load(loadCfg, pkgPath) 121 if err != nil { 122 return nil, fmt.Errorf("generating failed: %v", err) 123 } 124 125 if len(pkgs) != 1 { 126 return nil, fmt.Errorf( 127 "generate only allowed for one package at a time, found %d", 128 len(pkgs)) 129 } 130 131 g.pkg = pkgs[0] 132 if len(g.pkg.Errors) > 0 { 133 for _, err := range g.pkg.Errors { 134 g.addErr(err) 135 } 136 return nil, g.err 137 } 138 139 pkgName = g.pkg.Name 140 141 for _, obj := range g.pkg.TypesInfo.Defs { 142 if obj == nil || obj.Pkg() != g.pkg.Types || obj.Parent() == nil { 143 continue 144 } 145 g.typeMap[obj.Name()] = obj.Type() 146 } 147 } 148 149 // TODO: add package doc if there is no existing Go package or if it doesn't 150 // have package documentation already. 151 g.exec(headerCode, map[string]string{ 152 "pkgName": pkgName, 153 }) 154 155 iter, err := inst.Value().Fields(cue.Definitions(true)) 156 g.addErr(err) 157 158 for iter.Next() { 159 g.decl(iter.Label(), iter.Value()) 160 } 161 162 r := value.ConvertToRuntime(inst.Value().Context()) 163 b, err = r.Marshal(inst) 164 g.addErr(err) 165 166 g.exec(loadCode, map[string]string{ 167 "runtime": g.RuntimeVar, 168 "prefix": strValue(g.Prefix, defaultPrefix), 169 "data": string(b), 170 }) 171 172 if g.err != nil { 173 return nil, g.err 174 } 175 176 b, err = format.Source(g.w.Bytes()) 177 if err != nil { 178 // Return bytes as well to allow analysis of the failed Go code. 179 return g.w.Bytes(), err 180 } 181 182 return b, err 183 } 184 185 type generator struct { 186 Config 187 pkg *packages.Package 188 typeMap map[string]types.Type 189 190 w bytes.Buffer 191 err errors.Error 192 } 193 194 func (g *generator) addErr(err error) { 195 if err != nil { 196 g.err = errors.Append(g.err, errors.Promote(err, "generate failed")) 197 } 198 } 199 200 func (g *generator) exec(t *template.Template, data interface{}) { 201 g.addErr(t.Execute(&g.w, data)) 202 } 203 204 func (g *generator) decl(name string, v cue.Value) { 205 attr := v.Attribute("go") 206 207 if !ast.IsExported(name) && attr.Err() != nil { 208 return 209 } 210 211 goName := name 212 switch s, _ := attr.String(0); s { 213 case "": 214 case "-": 215 return 216 default: 217 goName = s 218 } 219 220 goTypeName := goName 221 goType := "" 222 if str, ok, _ := attr.Lookup(1, "type"); ok { 223 goType = str 224 goTypeName = str 225 } 226 227 isFunc, _ := attr.Flag(1, "func") 228 if goTypeName != goName { 229 isFunc = true 230 } 231 232 zero := "nil" 233 234 typ, ok := g.typeMap[goTypeName] 235 if !ok && !mappedGoTypes(goTypeName) { 236 return 237 } 238 if goType == "" { 239 goType = goTypeName 240 if typ != nil { 241 switch typ.Underlying().(type) { 242 case *types.Struct, *types.Array: 243 goType = "*" + goTypeName 244 zero = fmt.Sprintf("&%s{}", goTypeName) 245 case *types.Pointer: 246 zero = fmt.Sprintf("%s(nil)", goTypeName) 247 isFunc = true 248 } 249 } 250 } 251 252 g.exec(stubCode, map[string]interface{}{ 253 "prefix": strValue(g.Prefix, defaultPrefix), 254 "cueName": name, // the field name of the CUE type 255 "goType": goType, // the receiver or argument type 256 "zero": zero, // the zero value of the underlying type 257 258 // @go attribute options 259 "func": isFunc, 260 "validate": lookupName(attr, "validate", strValue(g.ValidateName, "Validate")), 261 "complete": lookupName(attr, "complete", g.CompleteName), 262 }) 263 } 264 265 func lookupName(attr cue.Attribute, option, config string) string { 266 name, ok, _ := attr.Lookup(1, option) 267 if !ok { 268 name = config 269 } 270 if name == "-" { 271 return "" 272 } 273 return name 274 } 275 276 func strValue(have, fallback string) string { 277 if have == "" { 278 return fallback 279 } 280 return have 281 } 282 283 func mappedGoTypes(s string) bool { 284 switch s { 285 case "bool", "float32", "float64", 286 "int", "int8", "int16", "int32", "int64", "string", 287 "uint", "uint8", "uint16", "uint32", "uint64": 288 return true 289 } 290 return false 291 }