Skip to content
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

openapi3gen: add CreateComponentSchemas option to export object schemas to components #935

Merged
merged 1 commit into from
Apr 6, 2024
Merged
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
Empty file.
14 changes: 14 additions & 0 deletions .github/docs/openapi3gen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type ExcludeSchemaSentinel struct{}

func (err *ExcludeSchemaSentinel) Error() string

type ExportComponentSchemasOptions struct {
ExportComponentSchemas bool
ExportTopLevelSchema bool
ExportGenerics bool
}

type Generator struct {
Types map[reflect.Type]*openapi3.SchemaRef

Expand All @@ -50,6 +56,12 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch
type Option func(*generatorOpt)
Option allows tweaking SchemaRef generation

func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option
CreateComponents changes the default behavior to add all schemas as
components Reduces duplicate schemas in routes

func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option

func SchemaCustomizer(sc SchemaCustomizerFn) Option
SchemaCustomizer allows customization of the schema that is generated for a
field, for example to support an additional tagging scheme
Expand Down Expand Up @@ -77,3 +89,5 @@ type SetSchemar interface {
their specification. Useful when some custom datatype is needed and/or some
custom logic is needed on how the schema values would be generated

type TypeNameGenerator func(t reflect.Type) string

3 changes: 2 additions & 1 deletion docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ set -o pipefail

outdir=.github/docs
mkdir -p "$outdir"
for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|cmd/'); do
for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|internal|cmd/'); do
echo $pkgpath
go doc -all ./"$pkgpath" | tee "$outdir/${pkgpath////_}.txt"
done

Expand Down
5 changes: 5 additions & 0 deletions openapi3gen/internal/subpkg/sub_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package subpkg

type Child struct {
Name string `yaml:"name"`
}
85 changes: 78 additions & 7 deletions openapi3gen/openapi3gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"math"
"reflect"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -42,10 +43,20 @@ type SetSchemar interface {
SetSchema(*openapi3.Schema)
}

type ExportComponentSchemasOptions struct {
ExportComponentSchemas bool
ExportTopLevelSchema bool
ExportGenerics bool
}

type TypeNameGenerator func(t reflect.Type) string

type generatorOpt struct {
useAllExportedFields bool
throwErrorOnCycle bool
schemaCustomizer SchemaCustomizerFn
useAllExportedFields bool
throwErrorOnCycle bool
schemaCustomizer SchemaCustomizerFn
exportComponentSchemas ExportComponentSchemasOptions
typeNameGenerator TypeNameGenerator
}

// UseAllExportedFields changes the default behavior of only
Expand All @@ -54,6 +65,10 @@ func UseAllExportedFields() Option {
return func(x *generatorOpt) { x.useAllExportedFields = true }
}

func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option {
return func(x *generatorOpt) { x.typeNameGenerator = tngnrt }
}

// ThrowErrorOnCycle changes the default behavior of creating cycle
// refs to instead error if a cycle is detected.
func ThrowErrorOnCycle() Option {
Expand All @@ -66,6 +81,13 @@ func SchemaCustomizer(sc SchemaCustomizerFn) Option {
return func(x *generatorOpt) { x.schemaCustomizer = sc }
}

// CreateComponents changes the default behavior
// to add all schemas as components
// Reduces duplicate schemas in routes
func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option {
return func(x *generatorOpt) { x.exportComponentSchemas = exso }
}

// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...)
func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) {
g := NewGenerator(opts...)
Expand All @@ -83,6 +105,7 @@ type Generator struct {
SchemaRefs map[*openapi3.SchemaRef]int

// componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles
// or if we have specified create components schemas
componentSchemaRefs map[string]struct{}
}

Expand Down Expand Up @@ -111,9 +134,16 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch
return nil, err
}
for ref := range g.SchemaRefs {
if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil {
schemas[ref.Ref] = &openapi3.SchemaRef{
Value: ref.Value,
refName := ref.Ref
if g.opts.exportComponentSchemas.ExportComponentSchemas && strings.HasPrefix(refName, "#/components/schemas/") {
refName = strings.TrimPrefix(refName, "#/components/schemas/")
}

if _, ok := g.componentSchemaRefs[refName]; ok && schemas != nil {
if ref.Value != nil && ref.Value.Properties != nil {
schemas[refName] = &openapi3.SchemaRef{
Value: ref.Value,
}
}
}
if strings.HasPrefix(ref.Ref, "#/components/schemas/") {
Expand Down Expand Up @@ -298,6 +328,14 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
schema.Type = &openapi3.Types{"string"}
schema.Format = "date-time"
} else {
typeName := g.generateTypeName(t)

if _, ok := g.componentSchemaRefs[typeName]; ok && g.opts.exportComponentSchemas.ExportComponentSchemas {
// Check if we have already parsed this component schema ref based on the name of the struct
// and use that if so
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
}

for _, fieldInfo := range typeInfo.Fields {
// Only fields with JSON tag are considered (by default)
if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
Expand Down Expand Up @@ -347,6 +385,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
g.SchemaRefs[ref]++
schema.WithPropertyRef(fieldName, ref)
}

}

// Object only if it has properties
Expand All @@ -362,6 +401,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
v.SetSchema(schema)
}
}

}

if g.opts.schemaCustomizer != nil {
Expand All @@ -370,9 +410,40 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
}
}

if !g.opts.exportComponentSchemas.ExportComponentSchemas || t.Kind() != reflect.Struct {
return openapi3.NewSchemaRef(t.Name(), schema), nil
}

// Best way I could find to check that
// this current type is a generic
isGeneric, err := regexp.Match(`^.*\[.*\]$`, []byte(t.Name()))
if err != nil {
return nil, err
}

if isGeneric && !g.opts.exportComponentSchemas.ExportGenerics {
return openapi3.NewSchemaRef(t.Name(), schema), nil
}

// For structs we add the schemas to the component schemas
if len(parents) > 1 || g.opts.exportComponentSchemas.ExportTopLevelSchema {
typeName := g.generateTypeName(t)

g.componentSchemaRefs[typeName] = struct{}{}
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
}

return openapi3.NewSchemaRef(t.Name(), schema), nil
}

func (g *Generator) generateTypeName(t reflect.Type) string {
if g.opts.typeNameGenerator != nil {
return g.opts.typeNameGenerator(t)
}

return t.Name()
}

func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef {
var typeName string
switch t.Kind() {
Expand All @@ -391,7 +462,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche
mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref}
return openapi3.NewSchemaRef("", mapSchema)
default:
typeName = t.Name()
typeName = g.generateTypeName(t)
}

g.componentSchemaRefs[typeName] = struct{}{}
Expand Down
Loading
Loading