Repository: swaggest/openapi-go
Pull request #75 (2023-08-03 13:16:06)

Add more controls to ContentUnit

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

| Language | Files |   Lines    |    Code    | Comments |  Blanks   | Complexity |     Bytes     |
|----------|-------|------------|------------|----------|-----------|------------|---------------|
| Go       | 14    | 2175 (+51) | 1589 (+35) | 139 (+5) | 447 (+11) | 423 (+11)  | 51.1K (+1.3K) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/openapi-go
## compatible changes
ContentUnit.IsDefault: added
ContentUnitPreparer: added
WithContentType: added
WithHTTPStatus: added

# summary
Inferred base version: v0.2.36
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/openapi-go/pull/75?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#75](https://app.codecov.io/gh/swaggest/openapi-go/pull/75?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (b6fef4e) into [master](https://app.codecov.io/gh/swaggest/openapi-go/commit/56b9d50f0a0cd1cab00d0ce0c9fea19601a4a90d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (56b9d50) will **decrease** coverage by `0.19%`.
> The diff coverage is `21.05%`.

```diff
@@            Coverage Diff             @@
##           master      #75      +/-   ##
==========================================
- Coverage   43.23%   43.04%   -0.19%     
==========================================
  Files           8        8              
  Lines        3819     3833      +14     
==========================================
- Hits         1651     1650       -1     
- Misses       1859     1872      +13     
- Partials      309      311       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `43.04% <21.05%> (-0.19%)` | :arrow_down: |

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.

| [Files Changed](https://app.codecov.io/gh/swaggest/openapi-go/pull/75?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [operation.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/75?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlcmF0aW9uLmdv) | `0.00% <0.00%> (ø)` | |
| [openapi3/reflect.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/75?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `78.92% <26.66%> (-2.24%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
diff --git a/internal/operation_context.go b/internal/operation_context.go
index 2a58a99..30e0ad1 100644
--- a/internal/operation_context.go
+++ b/internal/operation_context.go
@@ -75,7 +75,12 @@ func (o *OperationContext) SetPathPattern(pattern string) {
 // AddReqStructure adds request content schema.
 func (o *OperationContext) AddReqStructure(s interface{}, options ...openapi.ContentOption) {
 	c := openapi.ContentUnit{}
-	c.Structure = s
+
+	if cp, ok := s.(openapi.ContentUnitPreparer); ok {
+		cp.SetupContentUnit(&c)
+	} else {
+		c.Structure = s
+	}
 
 	for _, o := range options {
 		o(&c)
@@ -87,7 +92,12 @@ func (o *OperationContext) AddReqStructure(s interface{}, options ...openapi.Con
 // AddRespStructure adds response content schema.
 func (o *OperationContext) AddRespStructure(s interface{}, options ...openapi.ContentOption) {
 	c := openapi.ContentUnit{}
-	c.Structure = s
+
+	if cp, ok := s.(openapi.ContentUnitPreparer); ok {
+		cp.SetupContentUnit(&c)
+	} else {
+		c.Structure = s
+	}
 
 	for _, o := range options {
 		o(&c)
diff --git a/openapi3/helper.go b/openapi3/helper.go
index d4571f8..0b45d37 100644
--- a/openapi3/helper.go
+++ b/openapi3/helper.go
@@ -106,7 +106,7 @@ func (s *Spec) AddOperation(method, path string, operation Operation) error {
 	}
 
 	// Add "No Content" response if there are no responses configured.
-	if len(operation.Responses.MapOfResponseOrRefValues) == 0 {
+	if len(operation.Responses.MapOfResponseOrRefValues) == 0 && operation.Responses.Default == nil {
 		operation.Responses.WithMapOfResponseOrRefValuesItem(strconv.Itoa(http.StatusNoContent), ResponseOrRef{
 			Response: &Response{
 				Description: http.StatusText(http.StatusNoContent),
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index 48fe3f2..70820c6 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -267,6 +267,10 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 		default:
 			r.stringRequestBody(o, cu.ContentType, cu.Format)
 		}
+
+		if cu.Description != "" && o.RequestBody != nil && o.RequestBody.RequestBody != nil {
+			o.RequestBody.RequestBody.WithDescription(cu.Description)
+		}
 	}
 
 	return nil
@@ -690,8 +694,8 @@ func (r *Reflector) hasJSONBody(output interface{}) (bool, error) {
 
 func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) error {
 	for _, cu := range oc.Response() {
-		if cu.HTTPStatus == 0 {
-			cu.HTTPStatus = 200
+		if cu.HTTPStatus == 0 && !cu.IsDefault {
+			cu.HTTPStatus = http.StatusOK
 		}
 
 		cu.ContentType = strings.Split(cu.ContentType, ";")[0]
@@ -699,6 +703,20 @@ func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) err
 		httpStatus := strconv.Itoa(cu.HTTPStatus)
 		resp := o.Responses.MapOfResponseOrRefValues[httpStatus].Response
 
+		switch {
+		case cu.IsDefault:
+			httpStatus = "default"
+
+			if o.Responses.Default == nil {
+				o.Responses.Default = &ResponseOrRef{}
+			}
+
+			resp = o.Responses.Default.Response
+		case cu.HTTPStatus > 0 && cu.HTTPStatus < 6:
+			httpStatus = strconv.Itoa(cu.HTTPStatus) + "XX"
+			resp = o.Responses.MapOfResponseOrRefValues[httpStatus].Response
+		}
+
 		if resp == nil {
 			resp = &Response{}
 		}
@@ -721,13 +739,19 @@ func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) err
 			}
 		}
 
+		if cu.Description != "" {
+			resp.Description = cu.Description
+		}
+
 		if resp.Description == "" {
 			resp.Description = http.StatusText(cu.HTTPStatus)
 		}
 
-		o.Responses.WithMapOfResponseOrRefValuesItem(httpStatus, ResponseOrRef{
-			Response: resp,
-		})
+		if cu.IsDefault {
+			o.Responses.Default = &ResponseOrRef{Response: resp}
+		} else {
+			o.Responses.WithMapOfResponseOrRefValuesItem(httpStatus, ResponseOrRef{Response: resp})
+		}
 	}
 
 	return nil
diff --git a/operation.go b/operation.go
index a2378c3..c662336 100644
--- a/operation.go
+++ b/operation.go
@@ -27,14 +27,39 @@ type ContentOption func(cu *ContentUnit)
 
 // ContentUnit defines HTTP content.
 type ContentUnit struct {
-	Structure    interface{}
-	ContentType  string
-	Format       string
-	HTTPStatus   int
+	Structure   interface{}
+	ContentType string
+	Format      string
+
+	// HTTPStatus can have values 100-599 for single status, or 1-5 for status families (e.g. 2XX)
+	HTTPStatus int
+
+	// IsDefault indicates default response.
+	IsDefault bool
+
 	Description  string
 	fieldMapping map[In]map[string]string
 }
 
+// ContentUnitPreparer defines self-contained ContentUnit.
+type ContentUnitPreparer interface {
+	SetupContentUnit(cu *ContentUnit)
+}
+
+// WithContentType is a ContentUnit option.
+func WithContentType(contentType string) func(cu *ContentUnit) {
+	return func(cu *ContentUnit) {
+		cu.ContentType = contentType
+	}
+}
+
+// WithHTTPStatus is a ContentUnit option.
+func WithHTTPStatus(httpStatus int) func(cu *ContentUnit) {
+	return func(cu *ContentUnit) {
+		cu.HTTPStatus = httpStatus
+	}
+}
+
 // SetFieldMapping sets custom field mapping.
 func (c *ContentUnit) SetFieldMapping(in In, fieldToParamName map[string]string) {
 	if len(fieldToParamName) == 0 {


Test patch:
diff --git a/openapi3/_testdata/openapi_req.json b/openapi3/_testdata/openapi_req.json
index d106808..6d144c2 100644
--- a/openapi3/_testdata/openapi_req.json
+++ b/openapi3/_testdata/openapi_req.json
@@ -16,7 +16,7 @@
      {"name":"in_cookie","in":"cookie","deprecated":true,"schema":{"type":"string","deprecated":true}},
      {"name":"in_header","in":"header","schema":{"type":"number"}}
     ],
-    "requestBody":{"content":{"text/csv":{"schema":{"type":"string"}}}},
+    "requestBody":{"description":"Request body in CSV format.","content":{"text/csv":{"schema":{"type":"string"}}}},
     "responses":{"204":{"description":"No Content"}}
    }
   }
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 7231167..3e62f79 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -989,3 +989,42 @@ func TestReflector_AddOperation_request_queryObject_deepObject(t *testing.T) {
 	  }
 	}`, reflector.Spec)
 }
+
+type textCSV struct{}
+
+func (t textCSV) SetupContentUnit(cu *openapi.ContentUnit) {
+	cu.ContentType = "text/csv"
+	cu.Description = "This is CSV."
+	cu.IsDefault = true
+}
+
+func TestReflector_AddOperation_contentUnitPreparer(t *testing.T) {
+	r := openapi3.NewReflector()
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
+
+	oc.AddReqStructure(textCSV{})
+	oc.AddRespStructure(textCSV{})
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"requestBody":{
+			  "description":"This is CSV.",
+			  "content":{"text/csv":{"schema":{"type":"string"}}}
+			},
+			"responses":{
+			  "default":{
+				"description":"This is CSV.",
+				"content":{"text/csv":{"schema":{"type":"string"}}}
+			  }
+			}
+		  }
+		}
+	  }
+	}`, r.SpecSchema())
+}
diff --git a/operation_test.go b/operation_test.go
new file mode 100644
index 0000000..5e3c1a0
--- /dev/null
+++ b/operation_test.go
@@ -0,0 +1,18 @@
+package openapi_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/openapi-go"
+)
+
+func TestContentUnit_options(t *testing.T) {
+	cu := openapi.ContentUnit{}
+	openapi.WithContentType("text/csv")(&cu)
+	openapi.WithHTTPStatus(http.StatusConflict)(&cu)
+
+	assert.Equal(t, "text/csv", cu.ContentType)
+	assert.Equal(t, http.StatusConflict, cu.HTTPStatus)
+}