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

Adding basic support for XML structure #328

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
35 changes: 28 additions & 7 deletions examples/petstore/lib/testdata/doc/openapi.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"detail": {
"description": "Human readable error message",
"nullable": true,
"type": "string"
"type": "string",
"xml": {
"name": "detail"
}
},
"errors": {
"items": {
Expand All @@ -26,27 +29,42 @@
"type": "object"
},
"nullable": true,
"type": "array"
"type": "array",
"xml": {
"name": "errors"
}
},
"instance": {
"nullable": true,
"type": "string"
"type": "string",
"xml": {
"name": "instance"
}
},
"status": {
"description": "HTTP status code",
"example": 403,
"nullable": true,
"type": "integer"
"type": "integer",
"xml": {
"name": "status"
}
},
"title": {
"description": "Short title of the error",
"nullable": true,
"type": "string"
"type": "string",
"xml": {
"name": "title"
}
},
"type": {
"description": "URL of the error type. Can be used to lookup the error in a documentation",
"nullable": true,
"type": "string"
"type": "string",
"xml": {
"name": "type"
}
}
},
"type": "object"
Expand Down Expand Up @@ -137,7 +155,10 @@
"description": "PetsError schema",
"properties": {
"message": {
"type": "string"
"type": "string",
"xml": {
"name": "message"
}
}
},
"type": "object"
Expand Down
124 changes: 7 additions & 117 deletions openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ import (

func NewOpenAPI() *OpenAPI {
desc := NewOpenApiSpec()

mp := NewMetadataParsers()
mp.InitializeMetadataParsers(DefaultParsers)

return &OpenAPI{
description: &desc,
generator: openapi3gen.NewGenerator(),
globalOpenAPIResponses: []openAPIResponse{},
metadataParsers: mp,
}
}

Expand All @@ -32,6 +37,7 @@ type OpenAPI struct {
description *openapi3.T
generator *openapi3gen.Generator
globalOpenAPIResponses []openAPIResponse
metadataParsers *MetadataParsers
}

func (openAPI *OpenAPI) Description() *openapi3.T {
Expand Down Expand Up @@ -402,129 +408,13 @@ func (openapi *OpenAPI) createSchema(key string, v any) *openapi3.SchemaRef {
schemaRef.Value.Description = descriptionable.Description()
}

parseStructTags(reflect.TypeOf(v), schemaRef)
openapi.metadataParsers.ParseStructTags(reflect.TypeOf(v), schemaRef)

openapi.Description().Components.Schemas[key] = schemaRef

return schemaRef
}

// parseStructTags parses struct tags and modifies the schema accordingly.
// t must be a struct type.
// It adds the following struct tags (tag => OpenAPI schema field):
// - description => description
// - example => example
// - json => nullable (if contains omitempty)
// - validate:
// - required => required
// - min=1 => min=1 (for integers)
// - min=1 => minLength=1 (for strings)
// - max=100 => max=100 (for integers)
// - max=100 => maxLength=100 (for strings)
func parseStructTags(t reflect.Type, schemaRef *openapi3.SchemaRef) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}

if t.Kind() != reflect.Struct {
return
}

for i := range t.NumField() {
field := t.Field(i)
if field.Anonymous {
fieldType := field.Type
parseStructTags(fieldType, schemaRef)
continue
}

jsonFieldName := field.Tag.Get("json")
jsonFieldName = strings.Split(jsonFieldName, ",")[0] // remove omitempty, etc
if jsonFieldName == "-" {
continue
}
if jsonFieldName == "" {
jsonFieldName = field.Name
}

property := schemaRef.Value.Properties[jsonFieldName]
if property == nil {
slog.Warn("Property not found in schema", "property", jsonFieldName)
continue
}
if field.Type.Kind() == reflect.Struct {
parseStructTags(field.Type, property)
}
propertyCopy := *property
propertyValue := *propertyCopy.Value

// Example
example, ok := field.Tag.Lookup("example")
if ok {
propertyValue.Example = example
if propertyValue.Type.Is(openapi3.TypeInteger) {
exNum, err := strconv.Atoi(example)
if err != nil {
slog.Warn("Example might be incorrect (should be integer)", "error", err)
}
propertyValue.Example = exNum
}
}

// Validation
validateTag, ok := field.Tag.Lookup("validate")
validateTags := strings.Split(validateTag, ",")
if ok && slices.Contains(validateTags, "required") {
schemaRef.Value.Required = append(schemaRef.Value.Required, jsonFieldName)
}
for _, validateTag := range validateTags {
if strings.HasPrefix(validateTag, "min=") {
min, err := strconv.Atoi(strings.Split(validateTag, "=")[1])
if err != nil {
slog.Warn("Min might be incorrect (should be integer)", "error", err)
}

if propertyValue.Type.Is(openapi3.TypeInteger) {
minPtr := float64(min)
propertyValue.Min = &minPtr
} else if propertyValue.Type.Is(openapi3.TypeString) {
//nolint:gosec // disable G115
propertyValue.MinLength = uint64(min)
}
}
if strings.HasPrefix(validateTag, "max=") {
max, err := strconv.Atoi(strings.Split(validateTag, "=")[1])
if err != nil {
slog.Warn("Max might be incorrect (should be integer)", "error", err)
}
if propertyValue.Type.Is(openapi3.TypeInteger) {
maxPtr := float64(max)
propertyValue.Max = &maxPtr
} else if propertyValue.Type.Is(openapi3.TypeString) {
//nolint:gosec // disable G115
maxPtr := uint64(max)
propertyValue.MaxLength = &maxPtr
}
}
}

// Description
description, ok := field.Tag.Lookup("description")
if ok {
propertyValue.Description = description
}
jsonTag, ok := field.Tag.Lookup("json")
if ok {
if strings.Contains(jsonTag, ",omitempty") {
propertyValue.Nullable = true
}
}
propertyCopy.Value = &propertyValue

schemaRef.Value.Properties[jsonFieldName] = &propertyCopy
}
}

type OpenAPIDescriptioner interface {
Description() string
}
Loading
Loading