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}