Skip to content

Add OpenAPIModelNamer and opt-in generator support #537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/openapi-gen/args/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type Args struct {
// by API linter. If specified, API rule violations will be printed to report file.
// Otherwise default value "-" will be used which indicates stdout.
ReportFilename string

// UseOpenAPIModelNames specifies the use of OpenAPI model names instead of
// Go '<package>.<type>' names for types in the OpenAPI spec.
UseOpenAPIModelNames bool
}

// New returns default arguments for the generator. Returning the arguments instead
Expand All @@ -58,6 +62,7 @@ func (args *Args) AddFlags(fs *pflag.FlagSet) {
"the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year")
fs.StringVarP(&args.ReportFilename, "report-filename", "r", args.ReportFilename,
"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.")
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.")
}

// Validate checks the given arguments.
Expand Down
1 change: 1 addition & 0 deletions pkg/generators/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func GetTargets(context *generator.Context, args *args.Args) []generator.Target
newOpenAPIGen(
args.OutputFile,
args.OutputPkg,
args.UseOpenAPIModelNames,
),
newAPIViolationGen(),
}
Expand Down
53 changes: 38 additions & 15 deletions pkg/generators/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,19 @@ const (
type openAPIGen struct {
generator.GoGenerator
// TargetPackage is the package that will get GetOpenAPIDefinitions function returns all open API definitions.
targetPackage string
imports namer.ImportTracker
targetPackage string
imports namer.ImportTracker
useOpenAPIModelNames bool
}

func newOpenAPIGen(outputFilename string, targetPackage string) generator.Generator {
func newOpenAPIGen(outputFilename string, targetPackage string, useOpenAPIModelNames bool) generator.Generator {
return &openAPIGen{
GoGenerator: generator.GoGenerator{
OutputFilename: outputFilename,
},
imports: generator.NewImportTrackerForPackage(targetPackage),
targetPackage: targetPackage,
imports: generator.NewImportTrackerForPackage(targetPackage),
targetPackage: targetPackage,
useOpenAPIModelNames: useOpenAPIModelNames,
}
}

Expand Down Expand Up @@ -179,7 +181,7 @@ func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error {
sw.Do("return map[string]$.OpenAPIDefinition|raw${\n", argsFromType(nil))

for _, t := range c.Order {
err := newOpenAPITypeWriter(sw, c).generateCall(t)
err := newOpenAPITypeWriter(sw, c, g.useOpenAPIModelNames).generateCall(t)
if err != nil {
return err
}
Expand All @@ -194,7 +196,7 @@ func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error {
func (g *openAPIGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
klog.V(5).Infof("generating for type %v", t)
sw := generator.NewSnippetWriter(w, c, "$", "$")
err := newOpenAPITypeWriter(sw, c).generate(t)
err := newOpenAPITypeWriter(sw, c, g.useOpenAPIModelNames).generate(t)
if err != nil {
return err
}
Expand Down Expand Up @@ -233,14 +235,16 @@ type openAPITypeWriter struct {
refTypes map[string]*types.Type
enumContext *enumContext
GetDefinitionInterface *types.Type
useOpenAPIModelNames bool
}

func newOpenAPITypeWriter(sw *generator.SnippetWriter, c *generator.Context) openAPITypeWriter {
func newOpenAPITypeWriter(sw *generator.SnippetWriter, c *generator.Context, useOpenAPIModelNames bool) openAPITypeWriter {
return openAPITypeWriter{
SnippetWriter: sw,
context: c,
refTypes: map[string]*types.Type{},
enumContext: newEnumContext(c),
SnippetWriter: sw,
context: c,
refTypes: map[string]*types.Type{},
enumContext: newEnumContext(c),
useOpenAPIModelNames: useOpenAPIModelNames,
}
}

Expand Down Expand Up @@ -339,8 +343,18 @@ func (g openAPITypeWriter) generateCall(t *types.Type) error {
// Only generate for struct type and ignore the rest
switch t.Kind {
case types.Struct:
if namer.IsPrivateGoName(t.Name.Name) { // skip private types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you encounter one of these types or is this just precautionary?

Copy link
Contributor Author

@jpbetz jpbetz May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int64Amount is leaking out from quantity into pkg/generated/openapi/zz_generated.openapi.go. A grep will show this. runtime.Object also shows up in that file.

return nil
}

args := argsFromType(t)
g.Do("\"$.$\": ", t.Name)

if g.useOpenAPIModelNames {
g.Do("$.|raw${}.OpenAPIModelName(): ", t)
} else {
// Legacy case: use the "canonical type name"
g.Do("\"$.$\": ", t.Name)
}

hasV2Definition := hasOpenAPIDefinitionMethod(t)
hasV2DefinitionTypeAndFormat := hasOpenAPIDefinitionMethods(t)
Expand Down Expand Up @@ -667,7 +681,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error {
if len(deps) > 0 {
g.Do("Dependencies: []string{\n", args)
for _, k := range deps {
g.Do("\"$.$\",", k)
t := g.refTypes[k]
if g.useOpenAPIModelNames {
g.Do("$.|raw${}.OpenAPIModelName(),", t)
} else {
g.Do("\"$.$\",", k)
}
}
g.Do("},\n", nil)
}
Expand Down Expand Up @@ -1027,7 +1046,11 @@ func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) {

func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) {
g.refTypes[t.Name.String()] = t
g.Do("Ref: ref(\"$.$\"),\n", t.Name.String())
if g.useOpenAPIModelNames {
g.Do("Ref: ref($.|raw${}.OpenAPIModelName()),\n", t)
} else {
g.Do("Ref: ref(\"$.$\"),\n", t.Name.String())
}
}

func resolvePtrType(t *types.Type) *types.Type {
Expand Down
4 changes: 2 additions & 2 deletions pkg/generators/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ func testOpenAPITypeWriter(t *testing.T, cfg *packages.Config) (error, error, *b

callBuffer := &bytes.Buffer{}
callSW := generator.NewSnippetWriter(callBuffer, context, "$", "$")
callError := newOpenAPITypeWriter(callSW, context).generateCall(blahT)
callError := newOpenAPITypeWriter(callSW, context, false).generateCall(blahT)

funcBuffer := &bytes.Buffer{}
funcSW := generator.NewSnippetWriter(funcBuffer, context, "$", "$")
funcError := newOpenAPITypeWriter(funcSW, context).generate(blahT)
funcError := newOpenAPITypeWriter(funcSW, context, false).generate(blahT)

return callError, funcError, callBuffer, funcBuffer, imports.ImportLines()
}
Expand Down
13 changes: 12 additions & 1 deletion pkg/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,21 @@ type OpenAPICanonicalTypeNamer interface {
OpenAPICanonicalTypeName() string
}

// OpenAPIModelNamer is an interface Go types may implement to provide an OpenAPI model name.
//
// This takes precedence over OpenAPICanonicalTypeNamer, and should be used when a Go type has a model
// name that differs from its canonical type name as determined by Go package name reflection.
type OpenAPIModelNamer interface {
OpenAPIModelName() string
}

// GetCanonicalTypeName will find the canonical type name of a sample object, removing
// the "vendor" part of the path
func GetCanonicalTypeName(model interface{}) string {
if namer, ok := model.(OpenAPICanonicalTypeNamer); ok {
switch namer := model.(type) {
case OpenAPIModelNamer:
return namer.OpenAPIModelName()
case OpenAPICanonicalTypeNamer:
return namer.OpenAPICanonicalTypeName()
}
t := reflect.TypeOf(model)
Expand Down
43 changes: 43 additions & 0 deletions test/integration/integration_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,23 @@ var _ = BeforeSuite(func() {
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))

// Run the OpenAPI code generator with --use-openapi-model-names
Expect(terr).ShouldNot(HaveOccurred())

By("'namedmodels' running openapi-gen")
args = append([]string{
"--output-dir", tempDir + "/namedmodels",
"--output-pkg", outputPkg + "/namedmodels",
"--output-file", generatedCodeFileName,
"--use-openapi-model-names",
"--go-header-file", headerFilePath,
}, path.Join(testPkgRoot, "namedmodels"))
command = exec.Command(openAPIGenPath, args...)
command.Dir = workingDirectory
session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))

By("writing swagger v2.0")
// Create the OpenAPI swagger builder.
binaryPath, berr = gexec.Build("./builder/main.go")
Expand Down Expand Up @@ -131,6 +148,20 @@ var _ = BeforeSuite(func() {
session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))

By("'namedmodels' writing OpenAPI v3.0")
// Create the OpenAPI swagger builder.
binaryPath, berr = gexec.Build("./builder3/main.go")
Expect(berr).ShouldNot(HaveOccurred())

// Execute the builder, generating an OpenAPI swagger file with definitions.
gov3 = generatedFile("namedmodels/" + generatedOpenAPIv3FileName)
By("'namedmodels' writing swagger to " + gov3)
command = exec.Command(binaryPath, gov3)
command.Dir = workingDirectory
session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))
})

var _ = AfterSuite(func() {
Expand All @@ -152,6 +183,18 @@ var _ = Describe("Open API Definitions Generation", func() {
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))
})
It("'namedmodels' Generated code should match golden files", func() {
// Diff the generated code against the golden code. Exit code should be zero.
command := exec.Command(
"diff", "-u",
"pkg/generated/namedmodels/"+generatedCodeFileName,
generatedFile("namedmodels/"+generatedCodeFileName),
)
command.Dir = workingDirectory
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).ShouldNot(HaveOccurred())
Eventually(session, timeoutSeconds).Should(gexec.Exit(0))
})
})

Describe("Validating OpenAPI V2 Definition Generation", func() {
Expand Down
94 changes: 94 additions & 0 deletions test/integration/pkg/generated/namedmodels/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading