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 #914

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
27f199f
Add ability to export object schemas to components
EnriqueL8 Feb 22, 2024
89d9be2
feat: Add ability to ignore exporting top level schema
EnriqueL8 Feb 22, 2024
b5c51e1
Update docs
EnriqueL8 Feb 22, 2024
63a1f28
fix test format
EnriqueL8 Feb 22, 2024
2cc5392
type name builder
sskserk Feb 22, 2024
37ccde6
Merge pull request #2 from sskserk/type_name_builder
EnriqueL8 Feb 23, 2024
341c02f
Add docs
EnriqueL8 Feb 23, 2024
dbc7a0c
fix tests and sub type
EnriqueL8 Feb 23, 2024
0c6163e
Add support to ignore exporting generics
EnriqueL8 Feb 23, 2024
91ccfd6
Update docs
EnriqueL8 Feb 23, 2024
b95567d
renamed sub package; created test
sskserk Feb 23, 2024
a986d00
reformatted code
sskserk Feb 23, 2024
0a29359
Merge pull request #3 from sskserk/type_name_builder
EnriqueL8 Feb 26, 2024
1093a28
Update docs
EnriqueL8 Feb 26, 2024
749d258
extend generic test
EnriqueL8 Feb 26, 2024
9be388a
clean up
EnriqueL8 Feb 26, 2024
223fb55
format imports sequence
sskserk Feb 26, 2024
71785f8
Merge pull request #4 from sskserk/add_components_schemas
EnriqueL8 Feb 27, 2024
3436048
fix trimming for schema
EnriqueL8 Feb 28, 2024
0ac1691
Merge remote-tracking branch 'origin/master' into add_components_schemas
EnriqueL8 Feb 28, 2024
68678de
fix schema type
EnriqueL8 Feb 28, 2024
eb85980
fix check for object
EnriqueL8 Feb 28, 2024
e0111a1
Rename fields to export
EnriqueL8 Feb 29, 2024
a357586
fix docs
EnriqueL8 Feb 29, 2024
0883c33
fix: exporting schemas that are maps
EnriqueL8 Mar 4, 2024
7a9b932
address comment
sskserk Mar 25, 2024
98749de
Merge pull request #6 from sskserk/type_name_builder
EnriqueL8 Mar 28, 2024
cfcfe42
Review comments and fixes
EnriqueL8 Mar 28, 2024
3f39f82
Exclude subpkg docs
EnriqueL8 Mar 28, 2024
83b175e
Fix linting
EnriqueL8 Mar 28, 2024
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 All @@ -70,3 +82,5 @@ type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag,
A SchemaCustomizerFn can return an ExcludeSchemaSentinel error to indicate
that the schema for this field should not be included in the final output

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 @@ -35,10 +36,20 @@ type Option func(*generatorOpt)
// the final output
type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error

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 @@ -47,6 +58,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 @@ -59,6 +74,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 @@ -76,6 +98,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 @@ -104,9 +127,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 @@ -291,6 +321,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 @@ -340,13 +378,15 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
g.SchemaRefs[ref]++
schema.WithPropertyRef(fieldName, ref)
}

}

// Object only if it has properties
if schema.Properties != nil {
schema.Type = &openapi3.Types{"object"}
}
}

}

if g.opts.schemaCustomizer != nil {
Expand All @@ -355,9 +395,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()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about just checking that the last rune is ]?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

would be an array in that case?

Copy link
Collaborator

Choose a reason for hiding this comment

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

ah yes, then runes[-1] == ']' && runes[-2] != '['

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 @@ -376,7 +447,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