Skip to content

Commit d67c058

Browse files
authored
Merge pull request #563 from jpbetz/named-model-gen
Add model name generator
2 parents 66792ee + 11cd495 commit d67c058

File tree

11 files changed

+362
-65
lines changed

11 files changed

+362
-65
lines changed

cmd/openapi-gen/args/args.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ type Args struct {
3434
// Otherwise default value "-" will be used which indicates stdout.
3535
ReportFilename string
3636

37-
// UseOpenAPIModelNames specifies the use of OpenAPI model names instead of
38-
// Go '<package>.<type>' names for types in the OpenAPI spec.
39-
UseOpenAPIModelNames bool
37+
// OutputModelNameFile is the name of the file to be generated for OpenAPI schema name
38+
// accessor functions. If empty, no model name accessor functions are generated.
39+
// When this is specified, the OpenAPI spec generator will use the function names
40+
// instead of Go type names for schema names.
41+
OutputModelNameFile string
4042
}
4143

4244
// New returns default arguments for the generator. Returning the arguments instead
@@ -58,11 +60,17 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) {
5860
"the base Go import-path under which to generate results")
5961
fs.StringVar(&args.OutputFile, "output-file", "generated.openapi.go",
6062
"the name of the file to be generated")
63+
fs.StringVar(&args.OutputModelNameFile, "output-model-name-file", "",
64+
`The filename for generated model name accessor functions.
65+
If specified, a file with this name will be created in each package containing
66+
a "+k8s:openapi-model-package" tag. The generated functions return fully qualified
67+
model names, which are used in the OpenAPI spec as schema references instead of
68+
Go type names. If empty, no model name accessor functions are generated and names
69+
are inferred from Go type names.`)
6170
fs.StringVar(&args.GoHeaderFile, "go-header-file", "",
6271
"the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year")
6372
fs.StringVarP(&args.ReportFilename, "report-filename", "r", args.ReportFilename,
6473
"Name of report file used by API linter to print API violations. Default \"-\" stands for standard output. NOTE that if valid filename other than \"-\" is specified, API linter won't return error on detected API violations. This allows further check of existing API violations without stopping the OpenAPI generation toolchain.")
65-
fs.BoolVar(&args.UseOpenAPIModelNames, "use-openapi-model-names", false, "Use OpenAPI model names instead of Go '<package>.<type>' names for types in the OpenAPI spec.")
6674
}
6775

6876
// Validate checks the given arguments.

cmd/openapi-gen/openapi-gen.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"log"
2626

2727
"github.com/spf13/pflag"
28+
2829
"k8s.io/gengo/v2"
2930
"k8s.io/gengo/v2/generator"
3031
"k8s.io/klog/v2"
@@ -45,15 +46,35 @@ func main() {
4546
log.Fatalf("Arguments validation error: %v", err)
4647
}
4748

48-
myTargets := func(context *generator.Context) []generator.Target {
49-
return generators.GetTargets(context, args)
49+
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
50+
if err != nil {
51+
log.Fatalf("Failed loading boilerplate: %v", err)
52+
}
53+
54+
// Generates the code for model name accessors.
55+
if len(args.OutputModelNameFile) > 0 {
56+
modelNameTargets := func(context *generator.Context) []generator.Target {
57+
return generators.GetModelNameTargets(context, args, boilerplate)
58+
}
59+
if err := gengo.Execute(
60+
generators.NameSystems(),
61+
generators.DefaultNameSystem(),
62+
modelNameTargets,
63+
gengo.StdBuildTag,
64+
pflag.Args(),
65+
); err != nil {
66+
log.Fatalf("Model name code generation error: %v", err)
67+
}
5068
}
5169

5270
// Generates the code for the OpenAPIDefinitions.
71+
openAPITargets := func(context *generator.Context) []generator.Target {
72+
return generators.GetOpenAPITargets(context, args, boilerplate)
73+
}
5374
if err := gengo.Execute(
5475
generators.NameSystems(),
5576
generators.DefaultNameSystem(),
56-
myTargets,
77+
openAPITargets,
5778
gengo.StdBuildTag,
5879
pflag.Args(),
5980
); err != nil {

pkg/generators/config.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package generators
1919
import (
2020
"path"
2121

22-
"k8s.io/gengo/v2"
2322
"k8s.io/gengo/v2/generator"
2423
"k8s.io/gengo/v2/namer"
2524
"k8s.io/gengo/v2/types"
@@ -49,12 +48,8 @@ func DefaultNameSystem() string {
4948
return "sorting_namer"
5049
}
5150

52-
func GetTargets(context *generator.Context, args *args.Args) []generator.Target {
53-
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
54-
if err != nil {
55-
klog.Fatalf("Failed loading boilerplate: %v", err)
56-
}
57-
51+
// GetOpenAPITargets returns the targets for OpenAPI definition generation.
52+
func GetOpenAPITargets(context *generator.Context, args *args.Args, boilerplate []byte) []generator.Target {
5853
reportPath := "-"
5954
if args.ReportFilename != "" {
6055
reportPath = args.ReportFilename
@@ -74,7 +69,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target
7469
newOpenAPIGen(
7570
args.OutputFile,
7671
args.OutputPkg,
77-
args.UseOpenAPIModelNames,
72+
len(args.OutputModelNameFile) > 0,
7873
),
7974
newAPIViolationGen(),
8075
}
@@ -83,3 +78,56 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target
8378
},
8479
}
8580
}
81+
82+
// GetModelNameTargets returns the targets for model name generation.
83+
func GetModelNameTargets(context *generator.Context, args *args.Args, boilerplate []byte) []generator.Target {
84+
var targets []generator.Target
85+
for _, i := range context.Inputs {
86+
klog.V(5).Infof("Considering pkg %q", i)
87+
88+
pkg := context.Universe[i]
89+
90+
openAPISchemaNamePackage, err := extractOpenAPISchemaNamePackage(pkg.Comments)
91+
if err != nil {
92+
klog.Fatalf("Package %v: invalid %s:%v", i, tagModelPackage, err)
93+
}
94+
hasPackageTag := len(openAPISchemaNamePackage) > 0
95+
96+
hasCandidates := false
97+
for _, t := range pkg.Types {
98+
v, err := singularTag(tagModelPackage, t.CommentLines)
99+
if err != nil {
100+
klog.Fatalf("Type %v: invalid %s:%v", t.Name, tagModelPackage, err)
101+
}
102+
hasTag := hasPackageTag || v != nil
103+
hasModel := isSchemaNameType(t)
104+
if hasModel && hasTag {
105+
hasCandidates = true
106+
break
107+
}
108+
}
109+
if !hasCandidates {
110+
klog.V(5).Infof(" skipping package")
111+
continue
112+
}
113+
114+
klog.V(3).Infof("Generating package %q", pkg.Path)
115+
116+
targets = append(targets,
117+
&generator.SimpleTarget{
118+
PkgName: path.Base(pkg.Path),
119+
PkgPath: pkg.Path,
120+
PkgDir: pkg.Dir, // output pkg is the same as the input
121+
HeaderComment: boilerplate,
122+
FilterFunc: func(c *generator.Context, t *types.Type) bool {
123+
return t.Name.Package == pkg.Path
124+
},
125+
GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) {
126+
return []generator.Generator{
127+
NewSchemaNameGen(args.OutputModelNameFile, pkg.Path, openAPISchemaNamePackage),
128+
}
129+
},
130+
})
131+
}
132+
return targets
133+
}

pkg/generators/model_names.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package generators
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"strings"
23+
24+
"k8s.io/gengo/v2"
25+
"k8s.io/gengo/v2/generator"
26+
"k8s.io/gengo/v2/namer"
27+
"k8s.io/gengo/v2/types"
28+
"k8s.io/klog/v2"
29+
)
30+
31+
const (
32+
tagModelPackage = "k8s:openapi-model-package"
33+
)
34+
35+
func extractOpenAPISchemaNamePackage(comments []string) (string, error) {
36+
v, err := singularTag(tagModelPackage, comments)
37+
if v == nil || err != nil {
38+
return "", err
39+
}
40+
return v.Value, nil
41+
}
42+
43+
func singularTag(tagName string, comments []string) (*gengo.Tag, error) {
44+
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
45+
if err != nil {
46+
return nil, err
47+
}
48+
if len(tags) == 0 {
49+
return nil, nil
50+
}
51+
if len(tags) > 1 {
52+
return nil, fmt.Errorf("multiple %s tags found", tagName)
53+
}
54+
tag := tags[tagName]
55+
if len(tag) == 0 {
56+
return nil, nil
57+
}
58+
if len(tag) > 1 {
59+
klog.V(5).Infof("multiple %s tags found, using the first one", tagName)
60+
}
61+
value := tag[0]
62+
return &value, nil
63+
}
64+
65+
// genSchemaName produces a file with autogenerated openapi schema name functions.
66+
type genSchemaName struct {
67+
generator.GoGenerator
68+
targetPackage string
69+
imports namer.ImportTracker
70+
typesForInit []*types.Type
71+
openAPISchemaNamePackage string
72+
}
73+
74+
// NewSchemaNameGen creates a generator
75+
func NewSchemaNameGen(outputFilename, targetPackage string, openAPISchemaNamePackage string) generator.Generator {
76+
return &genSchemaName{
77+
GoGenerator: generator.GoGenerator{
78+
OutputFilename: outputFilename,
79+
},
80+
targetPackage: targetPackage,
81+
imports: generator.NewImportTracker(),
82+
typesForInit: make([]*types.Type, 0),
83+
openAPISchemaNamePackage: openAPISchemaNamePackage,
84+
}
85+
}
86+
87+
func (g *genSchemaName) Namers(c *generator.Context) namer.NameSystems {
88+
return namer.NameSystems{
89+
"public": namer.NewPublicNamer(1),
90+
"local": namer.NewPublicNamer(0),
91+
"raw": namer.NewRawNamer("", nil),
92+
}
93+
}
94+
95+
func (g *genSchemaName) Filter(c *generator.Context, t *types.Type) bool {
96+
// Filter out types not being processed or not copyable within the package.
97+
if !isSchemaNameType(t) {
98+
klog.V(2).Infof("Type %v is not a valid target for OpenAPI schema name", t)
99+
return false
100+
}
101+
g.typesForInit = append(g.typesForInit, t)
102+
return true
103+
}
104+
105+
// isSchemaNameType indicates whether or not a type could be used to serve an API.
106+
func isSchemaNameType(t *types.Type) bool {
107+
// Filter out private types.
108+
if namer.IsPrivateGoName(t.Name.Name) {
109+
return false
110+
}
111+
112+
for t.Kind == types.Alias {
113+
t = t.Underlying
114+
}
115+
116+
if t.Kind != types.Struct {
117+
return false
118+
}
119+
return true
120+
}
121+
122+
func (g *genSchemaName) isOtherPackage(pkg string) bool {
123+
if pkg == g.targetPackage {
124+
return false
125+
}
126+
if strings.HasSuffix(pkg, ""+g.targetPackage+"") {
127+
return false
128+
}
129+
return true
130+
}
131+
132+
func (g *genSchemaName) Imports(c *generator.Context) (imports []string) {
133+
importLines := []string{}
134+
for _, singleImport := range g.imports.ImportLines() {
135+
if g.isOtherPackage(singleImport) {
136+
importLines = append(importLines, singleImport)
137+
}
138+
}
139+
return importLines
140+
}
141+
142+
func (g *genSchemaName) Init(c *generator.Context, w io.Writer) error {
143+
return nil
144+
}
145+
146+
func (g *genSchemaName) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
147+
klog.V(3).Infof("Generating openapi schema name for type %v", t)
148+
149+
openAPISchemaNamePackage := g.openAPISchemaNamePackage
150+
v, err := singularTag(tagModelPackage, t.CommentLines)
151+
if err != nil {
152+
return fmt.Errorf("type %v: invalid %s:%v", t.Name, tagModelPackage, err)
153+
}
154+
if v != nil && v.Value != "" {
155+
openAPISchemaNamePackage = v.Value
156+
}
157+
158+
if openAPISchemaNamePackage == "" {
159+
return nil
160+
}
161+
162+
schemaName := openAPISchemaNamePackage + "." + t.Name.Name
163+
164+
a := map[string]interface{}{
165+
"type": t,
166+
"schemaName": schemaName,
167+
}
168+
169+
sw := generator.NewSnippetWriter(w, c, "$", "$")
170+
171+
sw.Do("// OpenAPIModelName returns the OpenAPI model name for this type.\n", a)
172+
sw.Do("func (in $.type|local$) OpenAPIModelName() string {\n", a)
173+
sw.Do("\treturn \"$.schemaName$\"\n", a)
174+
sw.Do("}\n\n", nil)
175+
176+
return sw.Error()
177+
}

0 commit comments

Comments
 (0)