diff --git a/go.mod b/go.mod index 396f5a9..955582f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/bool64/dev v0.2.10 github.com/stretchr/testify v1.7.1 github.com/swaggest/assertjson v1.6.8 - github.com/swaggest/jsonschema-go v0.3.32 + github.com/swaggest/jsonschema-go v0.3.33 github.com/swaggest/refl v1.0.2 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 746c0ee..168210d 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/swaggest/assertjson v1.6.8 h1:1O/9UI5M+2OJI7BeEWKGj0wTvpRXZt5FkOJ4nRkY4rA= github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M= -github.com/swaggest/jsonschema-go v0.3.32 h1:C8XiNEoR1L0QwBAWTvRyY0xYdEmEeJYthImF9GzSST0= -github.com/swaggest/jsonschema-go v0.3.32/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw= +github.com/swaggest/jsonschema-go v0.3.33 h1:G9FxhWHdINUXUshHwHr4X2ptdZWEyBBDu53+VY94oy0= +github.com/swaggest/jsonschema-go v0.3.33/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw= github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88= github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g= github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= diff --git a/openapi3/entities.go b/openapi3/entities.go index bca004d..74a725f 100644 --- a/openapi3/entities.go +++ b/openapi3/entities.go @@ -3627,8 +3627,7 @@ func (q QueryParameter) MarshalJSON() ([]byte, error) { // Header Parameter. // // Parameter in header. -type HeaderParameter struct { -} +type HeaderParameter struct{} // UnmarshalJSON decodes JSON. func (h *HeaderParameter) UnmarshalJSON(data []byte) error { @@ -3669,8 +3668,7 @@ func (h HeaderParameter) MarshalJSON() ([]byte, error) { // Cookie Parameter. // // Parameter in cookie. -type CookieParameter struct { -} +type CookieParameter struct{} // UnmarshalJSON decodes JSON. func (c *CookieParameter) UnmarshalJSON(data []byte) error { @@ -6755,8 +6753,7 @@ func (h HTTPSecurityScheme) MarshalJSON() ([]byte, error) { // Bearer. // // Bearer. -type Bearer struct { -} +type Bearer struct{} // UnmarshalJSON decodes JSON. func (b *Bearer) UnmarshalJSON(data []byte) error { diff --git a/openapi3/reflect.go b/openapi3/reflect.go index a9cb54d..e82eca1 100644 --- a/openapi3/reflect.go +++ b/openapi3/reflect.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "errors" "fmt" "mime/multipart" @@ -80,17 +81,20 @@ type OperationContext struct { HTTPStatus int RespContentType string RespHeaderMapping map[string]string + + ProcessingResponse bool + ProcessingIn string } // SetupRequest sets up operation parameters. func (r *Reflector) SetupRequest(oc OperationContext) error { return joinErrors( - r.parseParametersIn(oc.Operation, oc.Input, ParameterInQuery, oc.ReqQueryMapping), - r.parseParametersIn(oc.Operation, oc.Input, ParameterInPath, oc.ReqPathMapping), - r.parseParametersIn(oc.Operation, oc.Input, ParameterInCookie, oc.ReqCookieMapping), - r.parseParametersIn(oc.Operation, oc.Input, ParameterInHeader, oc.ReqHeaderMapping), - r.parseRequestBody(oc.Operation, oc.Input, tagJSON, mimeJSON, oc.HTTPMethod, nil), - r.parseRequestBody(oc.Operation, oc.Input, tagFormData, mimeFormUrlencoded, oc.HTTPMethod, oc.ReqFormDataMapping), + r.parseParametersIn(oc, ParameterInQuery, oc.ReqQueryMapping), + r.parseParametersIn(oc, ParameterInPath, oc.ReqPathMapping), + r.parseParametersIn(oc, ParameterInCookie, oc.ReqCookieMapping), + r.parseParametersIn(oc, ParameterInHeader, oc.ReqHeaderMapping), + r.parseRequestBody(oc, tagJSON, mimeJSON, oc.HTTPMethod, nil), + r.parseRequestBody(oc, tagFormData, mimeFormUrlencoded, oc.HTTPMethod, oc.ReqFormDataMapping), ) } @@ -120,8 +124,11 @@ type RequestBodyEnforcer interface { } func (r *Reflector) parseRequestBody( - o *Operation, input interface{}, tag, mime string, httpMethod string, mapping map[string]string, + oc OperationContext, tag, mime string, httpMethod string, mapping map[string]string, ) error { + o := oc.Operation + input := oc.Input + httpMethod = strings.ToUpper(httpMethod) _, forceRequestBody := input.(RequestBodyEnforcer) @@ -153,6 +160,7 @@ func (r *Reflector) parseRequestBody( } schema, err := r.Reflect(input, + r.withOperation(oc, false, "body"), jsonschema.DefinitionsPrefix("#/components/schemas/"+definitionPrefix), jsonschema.RootRef, jsonschema.PropertyNameMapping(mapping), @@ -217,13 +225,17 @@ const ( ) func (r *Reflector) parseParametersIn( - o *Operation, input interface{}, in ParameterIn, propertyMapping map[string]string, + oc OperationContext, in ParameterIn, propertyMapping map[string]string, ) error { + o := oc.Operation + input := oc.Input + if refl.IsSliceOrMap(input) { return nil } s, err := r.Reflect(input, + r.withOperation(oc, false, string(in)), jsonschema.DefinitionsPrefix("#/components/schemas/"), jsonschema.CollectDefinitions(r.collectDefinition), jsonschema.PropertyNameMapping(propertyMapping), @@ -265,6 +277,7 @@ func (r *Reflector) parseParametersIn( property := reflect.New(field.Type).Interface() if refl.HasTaggedFields(property, tagJSON) { propertySchema, err := r.Reflect(property, + r.withOperation(oc, false, string(in)), jsonschema.DefinitionsPrefix("#/components/schemas/"), jsonschema.CollectDefinitions(r.collectDefinition), jsonschema.RootRef, @@ -278,7 +291,9 @@ func (r *Reflector) parseParametersIn( p.Schema = nil p.WithContentItem("application/json", MediaType{Schema: &openapiSchema}) } else { - ps, err := r.Reflect(reflect.New(field.Type).Interface(), jsonschema.InlineRefs) + ps, err := r.Reflect(reflect.New(field.Type).Interface(), + r.withOperation(oc, false, string(in)), + jsonschema.InlineRefs) if err != nil { return err } @@ -339,10 +354,14 @@ func (r *Reflector) collectDefinition(name string, schema jsonschema.Schema) { r.SpecEns().ComponentsEns().SchemasEns().WithMapOfSchemaOrRefValuesItem(name, s) } -func (r *Reflector) parseResponseHeader(resp *Response, output interface{}, mapping map[string]string) error { +func (r *Reflector) parseResponseHeader(resp *Response, oc OperationContext) error { + output := oc.Output + mapping := oc.RespHeaderMapping + res := make(map[string]HeaderOrRef) schema, err := r.Reflect(output, + r.withOperation(oc, true, "header"), jsonschema.InlineRefs, jsonschema.PropertyNameMapping(mapping), jsonschema.PropertyNameTag("header"), @@ -418,12 +437,12 @@ func (r *Reflector) SetupResponse(oc OperationContext) error { if oc.Output != nil { oc.RespContentType = strings.Split(oc.RespContentType, ";")[0] - err := r.parseJSONResponse(&resp, oc.Output, oc.RespContentType) + err := r.parseJSONResponse(&resp, oc) if err != nil { return err } - err = r.parseResponseHeader(&resp, oc.Output, oc.RespHeaderMapping) + err = r.parseResponseHeader(&resp, oc) if err != nil { return err } @@ -456,13 +475,17 @@ func (r *Reflector) ensureResponseContentType(resp *Response, contentType string } } -func (r *Reflector) parseJSONResponse(resp *Response, output interface{}, contentType string) error { +func (r *Reflector) parseJSONResponse(resp *Response, oc OperationContext) error { + output := oc.Output + contentType := oc.RespContentType + // Check if output structure exposes meaningful schema. if hasJSONBody, err := r.hasJSONBody(output); err == nil && !hasJSONBody { return nil } schema, err := r.Reflect(output, + r.withOperation(oc, true, "body"), jsonschema.RootRef, jsonschema.DefinitionsPrefix("#/components/schemas/"), jsonschema.CollectDefinitions(r.collectDefinition), @@ -500,3 +523,23 @@ func (r *Reflector) parseJSONResponse(resp *Response, output interface{}, conten return nil } + +type ocCtxKey struct{} + +func (r *Reflector) withOperation(oc OperationContext, processingResponse bool, in string) func(rc *jsonschema.ReflectContext) { + return func(rc *jsonschema.ReflectContext) { + oc.ProcessingResponse = processingResponse + oc.ProcessingIn = in + + rc.Context = context.WithValue(rc.Context, ocCtxKey{}, oc) + } +} + +// OperationCtx retrieves operation context from reflect context. +func OperationCtx(rc *jsonschema.ReflectContext) (OperationContext, bool) { + if oc, ok := rc.Value(ocCtxKey{}).(OperationContext); ok { + return oc, true + } + + return OperationContext{}, false +} diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go index cd2d19e..cce669e 100644 --- a/openapi3/reflect_test.go +++ b/openapi3/reflect_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "reflect" "strconv" "testing" @@ -700,3 +701,55 @@ func TestReflector_SetupRequest_noBody(t *testing.T) { }`), oc.Operation) } } + +func TestOperationCtx(t *testing.T) { + type req struct { + Query string `query:"query"` + Header string `header:"header"` + Cookie string `cookie:"cookie"` + Path string `path:"path"` + Body string `json:"body"` + } + + type resp struct { + Header string `header:"header"` + Body string `json:"body"` + } + + r := openapi3.Reflector{} + oc := openapi3.OperationContext{ + Operation: &openapi3.Operation{}, + Input: new(req), + Output: new(resp), + } + + visited := map[string]bool{} + + r.DefaultOptions = append(r.DefaultOptions, func(rc *jsonschema.ReflectContext) { + it := rc.InterceptType + rc.InterceptType = func(value reflect.Value, schema *jsonschema.Schema) (bool, error) { + if occ, ok := openapi3.OperationCtx(rc); ok { + if occ.ProcessingResponse { + visited["resp:"+occ.ProcessingIn] = true + } else { + visited["req:"+occ.ProcessingIn] = true + } + } + + return it(value, schema) + } + }) + + require.NoError(t, r.SetupRequest(oc)) + require.NoError(t, r.SetupResponse(oc)) + + assert.Equal(t, map[string]bool{ + "req:body": true, + "req:cookie": true, + "req:header": true, + "req:path": true, + "req:query": true, + "resp:body": true, + "resp:header": true, + }, visited) +}