this repo has no description
at master 281 lines 7.8 kB view raw
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 15package gocode 16 17import ( 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) 31 32// Config defines options for generation Go code. 33type 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 52const 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// # Selection and Naming 78// 79// Generate will not generate any code for fields that have no go attribute 80// and that are not exported or for which there is no namesake Go type. 81// If the go attribute has the special value '-' as its name it will be dropped 82// as well. In all other cases Generate will generate Go code, even if the 83// resulting code will not compile. For instance, Generate will generate Go 84// code even if the user defines a Go type in the attribute that does not 85// exist. 86// 87// If a field selected for generation and the go name matches that the name of 88// the Go type, the corresponding validate and complete code are generated as 89// methods by default. If not, it will be generated as a function. The default 90// function name is the default operation name with the Go name as a suffix. 91// 92// Caveats 93// Currently not supported: 94// - option to generate Go structs (or automatically generate if undefined) 95// - for type option to refer to types outside the package. 96func Generate(pkgPath string, inst cue.InstanceOrValue, c *Config) (b []byte, err error) { 97 // TODO: if inst is nil, the instance is loaded from CUE files in the same 98 // package directory with the same package name. 99 if c == nil { 100 c = &Config{} 101 } 102 103 g := &generator{ 104 Config: *c, 105 typeMap: map[string]types.Type{}, 106 } 107 108 val := inst.Value() 109 pkgName := inst.Value().BuildInstance().PkgName 110 if pkgPath != "" { 111 loadCfg := &packages.Config{ 112 Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo, 113 } 114 pkgs, err := packages.Load(loadCfg, pkgPath) 115 if err != nil { 116 return nil, fmt.Errorf("generating failed: %v", err) 117 } 118 119 if len(pkgs) != 1 { 120 return nil, fmt.Errorf( 121 "generate only allowed for one package at a time, found %d", 122 len(pkgs)) 123 } 124 125 g.pkg = pkgs[0] 126 if len(g.pkg.Errors) > 0 { 127 for _, err := range g.pkg.Errors { 128 g.addErr(err) 129 } 130 return nil, g.err 131 } 132 133 pkgName = g.pkg.Name 134 135 for _, obj := range g.pkg.TypesInfo.Defs { 136 if obj == nil || obj.Pkg() != g.pkg.Types || obj.Parent() == nil { 137 continue 138 } 139 g.typeMap[obj.Name()] = obj.Type() 140 } 141 } 142 143 // TODO: add package doc if there is no existing Go package or if it doesn't 144 // have package documentation already. 145 g.exec(headerCode, map[string]string{ 146 "pkgName": pkgName, 147 }) 148 149 iter, err := val.Fields(cue.Definitions(true)) 150 g.addErr(err) 151 152 for iter.Next() { 153 // TODO(mvdan): using cue.Definitions above means that we iterate over definitions, 154 // whose selector will not be of string type. Revisit this, because using Unquoted 155 // for definitions is likely not right. 156 g.decl(iter.Selector().Unquoted(), iter.Value()) 157 } 158 159 r := (*cue.Runtime)(val.Context()) 160 b, err = r.Marshal(&val) 161 g.addErr(err) 162 163 g.exec(loadCode, map[string]string{ 164 "runtime": g.RuntimeVar, 165 "prefix": cmp.Or(g.Prefix, defaultPrefix), 166 "data": string(b), 167 }) 168 169 if g.err != nil { 170 return nil, g.err 171 } 172 173 b, err = format.Source(g.w.Bytes()) 174 if err != nil { 175 // Return bytes as well to allow analysis of the failed Go code. 176 return g.w.Bytes(), err 177 } 178 179 return b, err 180} 181 182type generator struct { 183 Config 184 pkg *packages.Package 185 typeMap map[string]types.Type 186 187 w bytes.Buffer 188 err errors.Error 189} 190 191func (g *generator) addErr(err error) { 192 if err != nil { 193 g.err = errors.Append(g.err, errors.Promote(err, "generate failed")) 194 } 195} 196 197func (g *generator) exec(t *template.Template, data interface{}) { 198 g.addErr(t.Execute(&g.w, data)) 199} 200 201func (g *generator) decl(name string, v cue.Value) { 202 attr := v.Attribute("go") 203 204 if !ast.IsExported(name) && attr.Err() != nil { 205 return 206 } 207 208 goName := name 209 switch s, _ := attr.String(0); s { 210 case "": 211 case "-": 212 return 213 default: 214 goName = s 215 } 216 217 goTypeName := goName 218 goType := "" 219 if str, ok, _ := attr.Lookup(1, "type"); ok { 220 goType = str 221 goTypeName = str 222 } 223 224 isFunc, _ := attr.Flag(1, "func") 225 if goTypeName != goName { 226 isFunc = true 227 } 228 229 zero := "nil" 230 231 typ, ok := g.typeMap[goTypeName] 232 if !ok && !mappedGoTypes(goTypeName) { 233 return 234 } 235 if goType == "" { 236 goType = goTypeName 237 if typ != nil { 238 switch typ.Underlying().(type) { 239 case *types.Struct, *types.Array: 240 goType = "*" + goTypeName 241 zero = fmt.Sprintf("&%s{}", goTypeName) 242 case *types.Pointer: 243 zero = fmt.Sprintf("%s(nil)", goTypeName) 244 isFunc = true 245 } 246 } 247 } 248 249 g.exec(stubCode, map[string]interface{}{ 250 "prefix": cmp.Or(g.Prefix, defaultPrefix), 251 "cueName": name, // the field name of the CUE type 252 "goType": goType, // the receiver or argument type 253 "zero": zero, // the zero value of the underlying type 254 255 // @go attribute options 256 "func": isFunc, 257 "validate": lookupName(attr, "validate", cmp.Or(g.ValidateName, "Validate")), 258 "complete": lookupName(attr, "complete", g.CompleteName), 259 }) 260} 261 262func lookupName(attr cue.Attribute, option, config string) string { 263 name, ok, _ := attr.Lookup(1, option) 264 if !ok { 265 name = config 266 } 267 if name == "-" { 268 return "" 269 } 270 return name 271} 272 273func mappedGoTypes(s string) bool { 274 switch s { 275 case "bool", "float32", "float64", 276 "int", "int8", "int16", "int32", "int64", "string", 277 "uint", "uint8", "uint16", "uint32", "uint64": 278 return true 279 } 280 return false 281}