cuelang.org/go@v0.10.1/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 "cmp" 20 "fmt" 21 "go/ast" 22 "go/format" 23 "go/types" 24 "text/template" 25 26 "golang.org/x/tools/go/packages" 27 28 "cuelang.org/go/cue" 29 "cuelang.org/go/cue/errors" 30 "cuelang.org/go/internal/value" 31 ) 32 33 // Config defines options for generation Go code. 34 type Config struct { 35 // Prefix is used as a prefix to all generated variables. It defaults to 36 // cuegen. 37 Prefix string 38 39 // ValidateName defines the default name for validation methods or prefix 40 // for validation functions. The default is "Validate". Set to "-" to 41 // disable generating validators. 42 ValidateName string 43 44 // CompleteName defines the default name for complete methods or prefix 45 // for complete functions. The default is "-" (disabled). 46 CompleteName string 47 48 // The cue.Runtime variable name to use for initializing Codecs. 49 // A new Runtime is created by default. 50 RuntimeVar string 51 } 52 53 const defaultPrefix = "cuegen" 54 55 // Generate generates Go code for the given instance in the directory of the 56 // given package. 57 // 58 // Generate converts top-level declarations to corresponding Go code. By default, 59 // it will only generate validation functions of methods for exported top-level 60 // declarations. The behavior can be altered with the @go attribute. 61 // 62 // The go attribute has the following form @go(<name>{,<option>}), where option 63 // is either a key-value pair or a flag. The name maps the CUE name to an 64 // alternative Go name. The special value '-' is used to indicate the field 65 // should be ignored for any Go generation. 66 // 67 // The following options are supported: 68 // 69 // type=<gotype> The Go type as which this value should be interpreted. 70 // This defaults to the type with the (possibly overridden) 71 // name of the field. 72 // validate=<name> Alternative name for the validation function or method 73 // Setting this to the empty string disables generation. 74 // complete=<name> Alternative name for the validation function or method. 75 // Setting this to the empty string disables generation. 76 // func Generate as a function instead of a method. 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 will 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 // Caveats 94 // Currently not supported: 95 // - option to generate Go structs (or automatically generate if undefined) 96 // - for type option to refer to types outside the package. 97 func Generate(pkgPath string, inst cue.InstanceOrValue, c *Config) (b []byte, err error) { 98 // TODO: if inst is nil, the instance is loaded from CUE files in the same 99 // package directory with the same package name. 100 if c == nil { 101 c = &Config{} 102 } 103 104 g := &generator{ 105 Config: *c, 106 typeMap: map[string]types.Type{}, 107 } 108 109 val := inst.Value() 110 pkgName := inst.Value().BuildInstance().PkgName 111 if pkgPath != "" { 112 loadCfg := &packages.Config{ 113 Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps, 114 } 115 pkgs, err := packages.Load(loadCfg, pkgPath) 116 if err != nil { 117 return nil, fmt.Errorf("generating failed: %v", err) 118 } 119 120 if len(pkgs) != 1 { 121 return nil, fmt.Errorf( 122 "generate only allowed for one package at a time, found %d", 123 len(pkgs)) 124 } 125 126 g.pkg = pkgs[0] 127 if len(g.pkg.Errors) > 0 { 128 for _, err := range g.pkg.Errors { 129 g.addErr(err) 130 } 131 return nil, g.err 132 } 133 134 pkgName = g.pkg.Name 135 136 for _, obj := range g.pkg.TypesInfo.Defs { 137 if obj == nil || obj.Pkg() != g.pkg.Types || obj.Parent() == nil { 138 continue 139 } 140 g.typeMap[obj.Name()] = obj.Type() 141 } 142 } 143 144 // TODO: add package doc if there is no existing Go package or if it doesn't 145 // have package documentation already. 146 g.exec(headerCode, map[string]string{ 147 "pkgName": pkgName, 148 }) 149 150 iter, err := val.Fields(cue.Definitions(true)) 151 g.addErr(err) 152 153 for iter.Next() { 154 g.decl(iter.Label(), iter.Value()) 155 } 156 157 r := value.ConvertToRuntime(val.Context()) 158 b, err = r.Marshal(&val) 159 g.addErr(err) 160 161 g.exec(loadCode, map[string]string{ 162 "runtime": g.RuntimeVar, 163 "prefix": cmp.Or(g.Prefix, defaultPrefix), 164 "data": string(b), 165 }) 166 167 if g.err != nil { 168 return nil, g.err 169 } 170 171 b, err = format.Source(g.w.Bytes()) 172 if err != nil { 173 // Return bytes as well to allow analysis of the failed Go code. 174 return g.w.Bytes(), err 175 } 176 177 return b, err 178 } 179 180 type generator struct { 181 Config 182 pkg *packages.Package 183 typeMap map[string]types.Type 184 185 w bytes.Buffer 186 err errors.Error 187 } 188 189 func (g *generator) addErr(err error) { 190 if err != nil { 191 g.err = errors.Append(g.err, errors.Promote(err, "generate failed")) 192 } 193 } 194 195 func (g *generator) exec(t *template.Template, data interface{}) { 196 g.addErr(t.Execute(&g.w, data)) 197 } 198 199 func (g *generator) decl(name string, v cue.Value) { 200 attr := v.Attribute("go") 201 202 if !ast.IsExported(name) && attr.Err() != nil { 203 return 204 } 205 206 goName := name 207 switch s, _ := attr.String(0); s { 208 case "": 209 case "-": 210 return 211 default: 212 goName = s 213 } 214 215 goTypeName := goName 216 goType := "" 217 if str, ok, _ := attr.Lookup(1, "type"); ok { 218 goType = str 219 goTypeName = str 220 } 221 222 isFunc, _ := attr.Flag(1, "func") 223 if goTypeName != goName { 224 isFunc = true 225 } 226 227 zero := "nil" 228 229 typ, ok := g.typeMap[goTypeName] 230 if !ok && !mappedGoTypes(goTypeName) { 231 return 232 } 233 if goType == "" { 234 goType = goTypeName 235 if typ != nil { 236 switch typ.Underlying().(type) { 237 case *types.Struct, *types.Array: 238 goType = "*" + goTypeName 239 zero = fmt.Sprintf("&%s{}", goTypeName) 240 case *types.Pointer: 241 zero = fmt.Sprintf("%s(nil)", goTypeName) 242 isFunc = true 243 } 244 } 245 } 246 247 g.exec(stubCode, map[string]interface{}{ 248 "prefix": cmp.Or(g.Prefix, defaultPrefix), 249 "cueName": name, // the field name of the CUE type 250 "goType": goType, // the receiver or argument type 251 "zero": zero, // the zero value of the underlying type 252 253 // @go attribute options 254 "func": isFunc, 255 "validate": lookupName(attr, "validate", cmp.Or(g.ValidateName, "Validate")), 256 "complete": lookupName(attr, "complete", g.CompleteName), 257 }) 258 } 259 260 func lookupName(attr cue.Attribute, option, config string) string { 261 name, ok, _ := attr.Lookup(1, option) 262 if !ok { 263 name = config 264 } 265 if name == "-" { 266 return "" 267 } 268 return name 269 } 270 271 func mappedGoTypes(s string) bool { 272 switch s { 273 case "bool", "float32", "float64", 274 "int", "int8", "int16", "int32", "int64", "string", 275 "uint", "uint8", "uint16", "uint32", "uint64": 276 return true 277 } 278 return false 279 }