Skip to content

Commit

Permalink
Add initial support for tag-less JSON requests/responses (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Dec 2, 2023
1 parent 1763c57 commit fb05447
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 67 deletions.
79 changes: 78 additions & 1 deletion internal/json_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package internal
import (
"bytes"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"reflect"
Expand All @@ -18,6 +19,9 @@ const (
tagJSON = "json"
tagFormData = "formData"
tagForm = "form"
tagHeader = "header"

componentsSchemas = "#/components/schemas/"
)

var defNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9.\-_]+`)
Expand Down Expand Up @@ -73,8 +77,19 @@ func ReflectRequestBody(
return nil, false, nil
}

// Checking for default options that allow tag-less JSON.
isProcessWithoutTags := false
_, err = r.Reflect("", func(rc *jsonschema.ReflectContext) {
isProcessWithoutTags = rc.ProcessWithoutTags
})

if err != nil {
return nil, false, fmt.Errorf("BUG: %w", err)
}

// JSON can be a map or array without field tags.
if !hasTaggedFields && len(mapping) == 0 && !refl.IsSliceOrMap(input) && refl.FindEmbeddedSliceOrMap(input) == nil {
if !hasTaggedFields && len(mapping) == 0 && !refl.IsSliceOrMap(input) &&
refl.FindEmbeddedSliceOrMap(input) == nil && !isProcessWithoutTags {
return nil, false, nil
}

Expand Down Expand Up @@ -192,3 +207,65 @@ func hasJSONBody(r *jsonschema.Reflector, output interface{}) (bool, error) {

return false, nil
}

// ReflectResponseHeader reflects response headers from content unit.
func ReflectResponseHeader(
r *jsonschema.Reflector,
oc openapi.OperationContext,
cu openapi.ContentUnit,
interceptProp jsonschema.InterceptPropFunc,
) (jsonschema.Schema, error) {
output := cu.Structure
mapping := cu.FieldMapping(openapi.InHeader)

if output == nil {
return jsonschema.Schema{}, nil
}

return r.Reflect(output,
func(rc *jsonschema.ReflectContext) {
rc.ProcessWithoutTags = false
},
openapi.WithOperationCtx(oc, true, openapi.InHeader),
jsonschema.InlineRefs,
jsonschema.PropertyNameMapping(mapping),
jsonschema.PropertyNameTag(tagHeader),
sanitizeDefName,
jsonschema.InterceptProp(interceptProp),
)
}

// ReflectParametersIn reflects JSON schema of request parameters.
func ReflectParametersIn(
r *jsonschema.Reflector,
oc openapi.OperationContext,
c openapi.ContentUnit,
in openapi.In,
collectDefinitions func(name string, schema jsonschema.Schema),
interceptProp jsonschema.InterceptPropFunc,
additionalTags ...string,
) (jsonschema.Schema, error) {
input := c.Structure
propertyMapping := c.FieldMapping(in)

if refl.IsSliceOrMap(input) {
return jsonschema.Schema{}, nil
}

return r.Reflect(input,
func(rc *jsonschema.ReflectContext) {
rc.ProcessWithoutTags = false
},
openapi.WithOperationCtx(oc, false, in),
jsonschema.DefinitionsPrefix(componentsSchemas),
jsonschema.CollectDefinitions(collectDefinitions),
jsonschema.PropertyNameMapping(propertyMapping),
jsonschema.PropertyNameTag(string(in), additionalTags...),
func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
},
sanitizeDefName,
jsonschema.SkipEmbeddedMapsSlices,
jsonschema.InterceptProp(interceptProp),
)
}
47 changes: 14 additions & 33 deletions openapi3/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,27 +380,17 @@ func (r *Reflector) parseParametersIn(
in openapi.In,
additionalTags ...string,
) error {
input := c.Structure
propertyMapping := c.FieldMapping(in)

if refl.IsSliceOrMap(input) {
if refl.IsSliceOrMap(c.Structure) {
return nil
}

definitionsPrefix := componentsSchemas

s, err := r.Reflect(input,
openapi.WithOperationCtx(oc, false, in),
jsonschema.DefinitionsPrefix(definitionsPrefix),
jsonschema.CollectDefinitions(r.collectDefinition()),
jsonschema.PropertyNameMapping(propertyMapping),
jsonschema.PropertyNameTag(string(in), additionalTags...),
func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
},
sanitizeDefName,
jsonschema.SkipEmbeddedMapsSlices,
jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error {
s, err := internal.ReflectParametersIn(
r.JSONSchemaReflector(),
oc,
c,
in,
r.collectDefinition(),
func(params jsonschema.InterceptPropParams) error {
if !params.Processed || len(params.Path) > 1 {
return nil
}
Expand Down Expand Up @@ -442,7 +432,7 @@ func (r *Reflector) parseParametersIn(
if refl.HasTaggedFields(property, tagJSON) && !refl.HasTaggedFields(property, string(in)) {
propertySchema, err := r.Reflect(property,
openapi.WithOperationCtx(oc, false, in),
jsonschema.DefinitionsPrefix(definitionsPrefix),
jsonschema.DefinitionsPrefix(componentsSchemas),
jsonschema.CollectDefinitions(r.collectDefinition()),
jsonschema.RootRef,
sanitizeDefName,
Expand Down Expand Up @@ -495,8 +485,7 @@ func (r *Reflector) parseParametersIn(
o.Parameters = append(o.Parameters, ParameterOrRef{Parameter: &p})

return nil
}),
)
}, additionalTags...)
if err != nil {
return err
}
Expand Down Expand Up @@ -532,22 +521,14 @@ func (r *Reflector) collectDefinition() func(name string, schema jsonschema.Sche
}

func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error {
output := cu.Structure
mapping := cu.FieldMapping(openapi.InHeader)

if output == nil {
if cu.Structure == nil {
return nil
}

res := make(map[string]HeaderOrRef)

schema, err := r.Reflect(output,
openapi.WithOperationCtx(oc, true, openapi.InHeader),
jsonschema.InlineRefs,
jsonschema.PropertyNameMapping(mapping),
jsonschema.PropertyNameTag(tagHeader),
sanitizeDefName,
jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error {
schema, err := internal.ReflectResponseHeader(r.JSONSchemaReflector(), oc, cu,
func(params jsonschema.InterceptPropParams) error {
if !params.Processed || len(params.Path) > 1 { // only top-level fields (including embedded).
return nil
}
Expand Down Expand Up @@ -579,7 +560,7 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont
}

return nil
}),
},
)
if err != nil {
return err
Expand Down
42 changes: 9 additions & 33 deletions openapi31/reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@ const (
tagJSON = "json"
tagFormData = "formData"
tagForm = "form"
tagHeader = "header"
mimeJSON = "application/json"
mimeFormUrlencoded = "application/x-www-form-urlencoded"
mimeMultipart = "multipart/form-data"
Expand Down Expand Up @@ -333,27 +332,12 @@ func (r *Reflector) parseParametersIn(
in openapi.In,
additionalTags ...string,
) error {
input := c.Structure
propertyMapping := c.FieldMapping(in)

if refl.IsSliceOrMap(input) {
if refl.IsSliceOrMap(c.Structure) {
return nil
}

definitionsPrefix := componentsSchemas

s, err := r.Reflect(input,
openapi.WithOperationCtx(oc, false, in),
jsonschema.DefinitionsPrefix(definitionsPrefix),
jsonschema.CollectDefinitions(r.collectDefinition()),
jsonschema.PropertyNameMapping(propertyMapping),
jsonschema.PropertyNameTag(string(in), additionalTags...),
func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
},
sanitizeDefName,
jsonschema.SkipEmbeddedMapsSlices,
jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error {
s, err := internal.ReflectParametersIn(
r.JSONSchemaReflector(), oc, c, in, r.collectDefinition(), func(params jsonschema.InterceptPropParams) error {
if !params.Processed || len(params.Path) > 1 {
return nil
}
Expand Down Expand Up @@ -393,7 +377,7 @@ func (r *Reflector) parseParametersIn(
if refl.HasTaggedFields(property, tagJSON) && !refl.HasTaggedFields(property, string(in)) { //nolint:nestif
propertySchema, err := r.Reflect(property,
openapi.WithOperationCtx(oc, false, in),
jsonschema.DefinitionsPrefix(definitionsPrefix),
jsonschema.DefinitionsPrefix(componentsSchemas),
jsonschema.CollectDefinitions(r.collectDefinition()),
jsonschema.RootRef,
sanitizeDefName,
Expand Down Expand Up @@ -449,7 +433,7 @@ func (r *Reflector) parseParametersIn(
o.Parameters = append(o.Parameters, ParameterOrReference{Parameter: &p})

return nil
}),
}, additionalTags...,
)
if err != nil {
return err
Expand Down Expand Up @@ -488,22 +472,14 @@ func (r *Reflector) collectDefinition() func(name string, schema jsonschema.Sche
}

func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error {
output := cu.Structure
mapping := cu.FieldMapping(openapi.InHeader)

if output == nil {
if cu.Structure == nil {
return nil
}

res := make(map[string]HeaderOrReference)

schema, err := r.Reflect(output,
openapi.WithOperationCtx(oc, true, openapi.InHeader),
jsonschema.InlineRefs,
jsonschema.PropertyNameMapping(mapping),
jsonschema.PropertyNameTag(tagHeader),
sanitizeDefName,
jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error {
schema, err := internal.ReflectResponseHeader(r.JSONSchemaReflector(), oc, cu,
func(params jsonschema.InterceptPropParams) error {
if !params.Processed || len(params.Path) > 1 { // only top-level fields (including embedded).
return nil
}
Expand Down Expand Up @@ -537,7 +513,7 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont
}

return nil
}),
},
)
if err != nil {
return err
Expand Down
53 changes: 53 additions & 0 deletions openapi31/reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1135,3 +1135,56 @@ func TestReflector_AddOperation_defName(t *testing.T) {
}
}`, r.Spec)
}

func Test_Repro2(t *testing.T) {
oarefl := openapi31.NewReflector()
oarefl.JSONSchemaReflector().DefaultOptions = append(oarefl.JSONSchemaReflector().DefaultOptions, jsonschema.ProcessWithoutTags)

{
var dummyIn struct {
ID int
}

var dummyOut struct {
Done bool
}

op, err := oarefl.NewOperationContext(http.MethodPost, "/postDelete")
if err != nil {
t.Fatal(err)
}

op.AddReqStructure(dummyIn, openapi.WithContentType("application/json"))
op.AddRespStructure(dummyOut, openapi.WithHTTPStatus(200))
if err = oarefl.AddOperation(op); err != nil {
t.Fatal(err)
}
}

assertjson.EqMarshal(t, `{
"openapi":"3.1.0","info":{"title":"","version":""},
"paths":{
"/postDelete":{
"post":{
"requestBody":{
"content":{
"application/json":{
"schema":{"properties":{"ID":{"type":"integer"}},"type":"object"}
}
}
},
"responses":{
"200":{
"description":"OK",
"content":{
"application/json":{
"schema":{"properties":{"Done":{"type":"boolean"}},"type":"object"}
}
}
}
}
}
}
}
}`, oarefl.SpecSchema())
}

0 comments on commit fb05447

Please sign in to comment.