Repository: swaggest/openapi-go
Pull request #34 (2022-04-19 10:40:29)

Expose OperationContext in reflect context hooks

github-actions[bot]: ### Lines Of Code

| Language  | Files |   Lines    |    Code    | Comments |  Blanks   | Complexity |     Bytes     |
|-----------|-------|------------|------------|----------|-----------|------------|---------------|
| Go        | 6     | 1252 (+43) | 939 (+32)  | 56 (+1)  | 257 (+10) | 294 (+1)   | 28.1K (+856B) |
| Go (gen)  | 1     | 8558 (-3)  | 5879 (-3)  | 719      | 1960      | 1246       | 190K (-6B)    |
| Go (test) | 5     | 1533 (+53) | 1113 (+45) | 228      | 192 (+8)  | 19 (+3)    | 39.5K (+1.2K) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/openapi-go/openapi3
## compatible changes
OperationContext.ProcessingIn: added
OperationContext.ProcessingResponse: added
OperationCtx: added

# summary
Inferred base version: v0.2.16
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:	(statements)	48.7%
<details><summary>Coverage diff with base branch</summary>

```diff
564,565c564,565
< github.com/swaggest/openapi-go/openapi3/reflect.go	parseRequestBody	97.5%
< github.com/swaggest/openapi-go/openapi3/reflect.go	parseParametersIn	83.7%
---
> github.com/swaggest/openapi-go/openapi3/reflect.go	parseRequestBody	97.6%
> github.com/swaggest/openapi-go/openapi3/reflect.go	parseParametersIn	84.3%
567c567
< github.com/swaggest/openapi-go/openapi3/reflect.go	parseResponseHeader	87.5%
---
> github.com/swaggest/openapi-go/openapi3/reflect.go	parseResponseHeader	88.9%
572c572,574
< github.com/swaggest/openapi-go/openapi3/reflect.go	parseJSONResponse	88.2%
---
> github.com/swaggest/openapi-go/openapi3/reflect.go	parseJSONResponse	89.5%
> github.com/swaggest/openapi-go/openapi3/reflect.go	withOperation	100.0%
> github.com/swaggest/openapi-go/openapi3/reflect.go	OperationCtx	100.0%
579c581
< total:	(statements)	48.5%
---
> total:	(statements)	48.7%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/openapi-go/pull/34?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#34](https://codecov.io/gh/swaggest/openapi-go/pull/34?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (63085ca) into [master](https://codecov.io/gh/swaggest/openapi-go/commit/d01f6e1934e1c0d1563002a5679dbae8911ff23d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (d01f6e1) will **increase** coverage by `0.37%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master      #34      +/-   ##
==========================================
+ Coverage   40.42%   40.79%   +0.37%     
==========================================
  Files           5        5              
  Lines        3518     3540      +22     
==========================================
+ Hits         1422     1444      +22     
  Misses       1802     1802              
  Partials      294      294              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `40.79% <100.00%> (+0.37%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/openapi-go/pull/34?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [openapi3/entities.go](https://codecov.io/gh/swaggest/openapi-go/pull/34/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvZW50aXRpZXMuZ28=) | `34.30% <ø> (ø)` | |
| [openapi3/reflect.go](https://codecov.io/gh/swaggest/openapi-go/pull/34/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `85.22% <100.00%> (+1.34%)` | :arrow_up: |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
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
+}


Test patch:
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)
+}