-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathgotypes.go
More file actions
361 lines (318 loc) · 11.4 KB
/
gotypes.go
File metadata and controls
361 lines (318 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
package jennies
import (
"bytes"
"fmt"
"go/format"
"go/parser"
"go/token"
"path"
"path/filepath"
"strings"
"cuelang.org/go/cue"
"github.com/dave/dst/decorator"
"github.com/dave/dst/dstutil"
"github.com/getkin/kin-openapi/openapi3"
"github.com/grafana/codejen"
"golang.org/x/tools/imports"
"github.com/grafana/grafana-app-sdk/codegen"
deepmapcodegen "github.com/grafana/grafana-app-sdk/internal/deepmap/oapi-codegen/pkg/codegen"
)
const GoTypesMaxDepth = 5
// GoTypes is a Jenny for turning a codegen.Kind into go types according to its codegen settings.
type GoTypes struct {
// GenerateOnlyCurrent should be set to true if you only want to generate code for the kind.Properties().Current version.
// This will affect the package and path(s) of the generated file(s).
GenerateOnlyCurrent bool
// Depth represents the tree depth for creating go types from fields. A Depth of 0 will return one go type
// (plus any definitions used by that type), a Depth of 1 will return a file with a go type for each top-level field
// (plus any definitions encompassed by each type), etc. Note that types are _not_ generated for fields above the Depth
// level--i.e. a Depth of 1 will generate go types for each field within the KindVersion.Schema, but not a type for the
// Schema itself. Because Depth results in recursive calls, the highest value is bound to a max of GoTypesMaxDepth.
Depth int
// NamingDepth determines how types are named in relation to Depth. If Depth <= NamingDepth, the go types are named
// using the field name of the type. Otherwise, Names used are prefixed by field names between Depth and NamingDepth.
// Typically, a value of 0 is "safest" for NamingDepth, as it prevents overlapping names for types.
// However, if you know that your fields have unique names up to a certain depth, you may configure this to be higher.
NamingDepth int
// AddKubernetesCodegen toggles whether kubernetes codegen comments are added to the go types generated by this jenny.
// The codegen comments can then be used with the OpenAPI jenny, or with the kubernetes codegen tooling.
AddKubernetesCodegen bool
// GroupByKind determines whether kinds are grouped by GroupVersionKind or just GroupVersion.
// If GroupByKind is true, generated paths are <kind>/<version>/<file>, instead of the default <version>/<file>.
// When GroupByKind is false, subresource types (such as spec and status) are prefixed with the kind name,
// i.e. generating FooSpec instead of Spec for kind.Name() = "Foo" and Depth=1
GroupByKind bool
}
func (*GoTypes) JennyName() string {
return "GoTypes"
}
func (g *GoTypes) Generate(kind codegen.Kind) (codejen.Files, error) {
if g.GenerateOnlyCurrent {
ver := kind.Version(kind.Properties().Current)
if ver == nil {
return nil, fmt.Errorf("version '%s' of kind '%s' does not exist", kind.Properties().Current, kind.Name())
}
return g.generateFiles(ver, kind.Name(), kind.Properties().MachineName, kind.Properties().MachineName, kind.Properties().MachineName)
}
files := make(codejen.Files, 0)
versions := kind.Versions()
for i := 0; i < len(versions); i++ {
ver := versions[i]
if !ver.Codegen.Backend {
continue
}
generated, err := g.generateFiles(&ver, kind.Name(), kind.Properties().MachineName, ToPackageName(ver.Version), GetGeneratedPath(g.GroupByKind, kind, ver.Version))
if err != nil {
return nil, err
}
files = append(files, generated...)
}
return files, nil
}
func (g *GoTypes) generateFiles(version *codegen.KindVersion, name string, machineName string, packageName string, pathPrefix string) (codejen.Files, error) {
if g.Depth > 0 {
namePrefix := ""
if !g.GroupByKind {
namePrefix = exportField(name)
}
return g.generateFilesAtDepth(version.Schema, version, 0, machineName, packageName, pathPrefix, namePrefix)
}
applyFuncs := make([]dstutil.ApplyFunc, 0)
if g.AddKubernetesCodegen {
applyFuncs = append(applyFuncs, addGenComments())
}
goBytes, err := GoTypesFromCUE(version.Schema, CUEGoConfig{
PackageName: packageName,
Name: exportField(sanitizeLabelString(name)),
Version: version.Version,
ApplyFuncs: applyFuncs,
}, 0)
if err != nil {
return nil, err
}
return codejen.Files{codejen.File{
Data: goBytes,
RelativePath: fmt.Sprintf(path.Join(pathPrefix, "%s_gen.go"), strings.ToLower(machineName)),
From: []codejen.NamedJenny{g},
}}, nil
}
//nolint:goconst
func (g *GoTypes) generateFilesAtDepth(v cue.Value, kv *codegen.KindVersion, currDepth int, machineName string, packageName string, pathPrefix string, namePrefix string) (codejen.Files, error) {
if currDepth == g.Depth {
fieldName := make([]string, 0)
for _, s := range TrimPathPrefix(v.Path(), kv.Schema.Path()).Selectors() {
fieldName = append(fieldName, s.String())
}
applyFuncs := make([]dstutil.ApplyFunc, 0)
if g.AddKubernetesCodegen && fieldName[len(fieldName)-1] != "metadata" { // metadata hack because of thema
applyFuncs = append(applyFuncs, addGenComments())
}
goBytes, err := GoTypesFromCUE(v, CUEGoConfig{
PackageName: packageName,
Name: exportField(strings.Join(fieldName, "")),
NamePrefix: namePrefix,
Version: kv.Version,
ApplyFuncs: applyFuncs,
}, len(v.Path().Selectors())-(g.Depth-g.NamingDepth))
if err != nil {
return nil, err
}
return codejen.Files{codejen.File{
Data: goBytes,
RelativePath: fmt.Sprintf(path.Join(pathPrefix, "%s_%s_gen.go"), strings.ToLower(machineName), strings.Join(fieldName, "_")),
From: []codejen.NamedJenny{g},
}}, nil
}
it, err := v.Fields()
if err != nil {
return nil, err
}
files := make(codejen.Files, 0)
for it.Next() {
f, err := g.generateFilesAtDepth(it.Value(), kv, currDepth+1, machineName, packageName, pathPrefix, namePrefix)
if err != nil {
return nil, err
}
files = append(files, f...)
}
return files, nil
}
type CUEGoConfig struct {
PackageName string
Name string
Version string
IgnoreDiscoveredImports bool
// ApplyFuncs is a slice of AST manipulation funcs that will be executed against
// the generated Go file prior to running it through goimports. For each slice
// element, [dstutil.Apply] is called with the element as the "pre" parameter.
ApplyFuncs []dstutil.ApplyFunc
// UseGoDeclInComments sets the name of the fields and structs at the beginning of each comment.
UseGoDeclInComments bool
// NamePrefix prefixes all generated types with the provided NamePrefix
NamePrefix string
}
func GoTypesFromCUE(v cue.Value, cfg CUEGoConfig, maxNamingDepth int) ([]byte, error) {
openAPIConfig := CUEOpenAPIConfig{
Name: cfg.NamePrefix + cfg.Name,
Version: cfg.Version,
NameFunc: func(_ cue.Value, path cue.Path) string {
i := 0
for ; i < len(path.Selectors()) && i < len(v.Path().Selectors()); i++ {
if maxNamingDepth > 0 && i >= maxNamingDepth {
break
}
if !SelEq(path.Selectors()[i], v.Path().Selectors()[i]) {
break
}
}
if i > 0 {
path = cue.MakePath(path.Selectors()[i:]...)
}
return cfg.NamePrefix + exportField(strings.Trim(path.String(), "?#"))
},
}
yml, err := CUEValueToOAPIYAML(v, openAPIConfig)
if err != nil {
return nil, err
}
loader := openapi3.NewLoader()
oT, err := loader.LoadFromData(yml)
if err != nil {
return nil, fmt.Errorf("loading generated openapi failed: %w", err)
}
ccfg := deepmapcodegen.Configuration{
PackageName: cfg.PackageName,
Compatibility: deepmapcodegen.CompatibilityOptions{
AlwaysPrefixEnumValues: true,
},
Generate: deepmapcodegen.GenerateOptions{
Models: true,
},
OutputOptions: deepmapcodegen.OutputOptions{
SkipPrune: true,
// SkipFmt: true, // we should be able to skip fmt, but dst's parser panics on nested structs when we don't
UserTemplates: map[string]string{
"imports.tmpl": importstmpl,
},
},
ImportMapping: nil,
AdditionalImports: nil,
}
gostr, err := deepmapcodegen.Generate(oT, ccfg)
if err != nil {
return nil, fmt.Errorf("openapi generation failed: %w", err)
}
applyFuncs := []dstutil.ApplyFunc{depointerizer(), fixRawData(), fixUnderscoreInTypeName()}
if !cfg.UseGoDeclInComments {
applyFuncs = append(applyFuncs, fixTODOComments())
}
applyFuncs = append(applyFuncs, cfg.ApplyFuncs...)
return postprocessGoFile(genGoFile{
path: fmt.Sprintf("%s_type_gen.go", cfg.Name),
appliers: applyFuncs,
in: []byte(gostr),
errifadd: !cfg.IgnoreDiscoveredImports,
})
}
type genGoFile struct {
errifadd bool
path string
appliers []dstutil.ApplyFunc
in []byte
}
func postprocessGoFile(cfg genGoFile) ([]byte, error) {
fname := sanitizeLabelString(filepath.Base(cfg.path))
buf := new(bytes.Buffer)
fset := token.NewFileSet()
gf, err := decorator.ParseFile(fset, fname, string(cfg.in), parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("error parsing generated file: %w", err)
}
for _, af := range cfg.appliers {
dstutil.Apply(gf, af, nil)
}
err = decorator.Fprint(buf, gf)
if err != nil {
return nil, fmt.Errorf("error formatting generated file: %w", err)
}
byt, err := imports.Process(fname, buf.Bytes(), nil)
if err != nil {
return nil, fmt.Errorf("goimports processing of generated file failed: %w", err)
}
if cfg.errifadd {
// Compare imports before and after; warn about performance if some were added
gfa, _ := parser.ParseFile(fset, fname, string(byt), parser.ParseComments)
imap := make(map[string]bool)
for _, im := range gf.Imports {
imap[im.Path.Value] = true
}
var added []string
for _, im := range gfa.Imports {
if !imap[im.Path.Value] {
added = append(added, im.Path.Value)
}
}
if len(added) != 0 {
// TODO improve the guidance in this error if/when we better abstract over imports to generate
return nil, fmt.Errorf("goimports added the following import statements to %s: \n\t%s\nRelying on goimports to find imports significantly slows down code generation. Either add these imports with an AST manipulation in cfg.ApplyFuncs, or set cfg.IgnoreDiscoveredImports to true", cfg.path, strings.Join(added, "\n\t"))
}
}
return format.Source(byt)
}
// SanitizeLabelString strips characters from a string that are not allowed for
// use in a CUE label.
func sanitizeLabelString(s string) string {
return strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
fallthrough
case r >= 'A' && r <= 'Z':
fallthrough
case r >= '0' && r <= '9':
fallthrough
case r == '_':
return r
default:
return -1
}
}, s)
}
// exportField makes a field name exported
func exportField(field string) string {
if len(field) > 0 {
return strings.ToUpper(field[:1]) + field[1:]
}
return strings.ToUpper(field)
}
// Almost all of the below imports are eliminated by dst transformers and calls
// to goimports - but if they're not present in the template, then the internal
// call to goimports that oapi-codegen makes will trigger a search for them,
// which can slow down codegen by orders of magnitude.
var importstmpl = `package {{ .PackageName }}
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"os"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/deepmap/oapi-codegen/pkg/runtime"
openapi_types "github.com/deepmap/oapi-codegen/pkg/types"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"
"github.com/labstack/echo/v4"
"github.com/gin-gonic/gin"
"github.com/gorilla/mux"
)
`