Repository: swaggest/rest
Pull request #155 (2023-04-26 13:13:57)

Implement support for multiple HTTP success statuses
When `OutputWithHTTPStatus` is implemented on the output port, `HTTPStatus()` defines the successful HTTP status of a given operation, and `ExpectedHTTPStatuses()` defines all expected HTTP statuses for API documentation purposes.

See #153 for the discussion about this feature.

codecov[bot]: ## [Codecov](https://codecov.io/gh/swaggest/rest/pull/155?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#155](https://codecov.io/gh/swaggest/rest/pull/155?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (2cc37cc) into [master](https://codecov.io/gh/swaggest/rest/commit/0e69b4946a9a717c8ce4d2b076f873552f7cac6e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (0e69b49) will **increase** coverage by `0.00%`.
> The diff coverage is `58.82%`.

```diff
@@           Coverage Diff           @@
##           master     #155   +/-   ##
=======================================
  Coverage   77.68%   77.68%           
=======================================
  Files          28       28           
  Lines        1600     1609    +9     
=======================================
+ Hits         1243     1250    +7     
- Misses        235      236    +1     
- Partials      122      123    +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.68% <58.82%> (+<0.01%)` | :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/rest/pull/155?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [response/encoder.go](https://codecov.io/gh/swaggest/rest/pull/155?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVzcG9uc2UvZW5jb2Rlci5nbw==) | `66.94% <50.00%> (+0.42%)` | :arrow_up: |
| [openapi/collector.go](https://codecov.io/gh/swaggest/rest/pull/155?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaS9jb2xsZWN0b3IuZ28=) | `64.26% <63.63%> (+0.05%)` | :arrow_up: |

: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)

vearutop: Thank you, looks good! 👍 

Patch:
diff --git a/openapi/collector.go b/openapi/collector.go
index 3070c88..b3ff2e9 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -142,17 +142,25 @@ func (c *Collector) setupOutput(oc *openapi3.OperationContext, u usecase.Interac
 		noContent = true
 	}
 
-	if oc.HTTPStatus == 0 {
-		oc.HTTPStatus = status
-	}
-
 	if !noContent && oc.RespContentType == "" {
 		oc.RespContentType = c.DefaultSuccessResponseContentType
 	}
 
-	err := c.Reflector().SetupResponse(*oc)
-	if err != nil {
-		return err
+	if outputWithStatus, ok := oc.Output.(rest.OutputWithHTTPStatus); ok {
+		for _, status := range outputWithStatus.ExpectedHTTPStatuses() {
+			oc.HTTPStatus = status
+			if err := c.Reflector().SetupResponse(*oc); err != nil {
+				return err
+			}
+		}
+	} else {
+		if oc.HTTPStatus == 0 {
+			oc.HTTPStatus = status
+		}
+		err := c.Reflector().SetupResponse(*oc)
+		if err != nil {
+			return err
+		}
 	}
 
 	if oc.HTTPMethod == http.MethodHead {
diff --git a/response.go b/response.go
index 5f74fbd..b01f4f8 100644
--- a/response.go
+++ b/response.go
@@ -11,3 +11,9 @@ type ETagged interface {
 type JSONWriterTo interface {
 	JSONWriteTo(w io.Writer) (int, error)
 }
+
+// OutputWithHTTPStatus exposes HTTP status code(s) for output.
+type OutputWithHTTPStatus interface {
+	HTTPStatus() int
+	ExpectedHTTPStatuses() []int
+}
diff --git a/response/encoder.go b/response/encoder.go
index d8c3eb3..8b0cdb3 100644
--- a/response/encoder.go
+++ b/response/encoder.go
@@ -191,10 +191,14 @@ func (h *Encoder) SetupOutput(output interface{}, ht *rest.HandlerTrait) {
 		return
 	}
 
-	if h.skipRendering && !h.outputWithWriter {
-		ht.SuccessStatus = http.StatusNoContent
+	if outputWithStatus, ok := output.(rest.OutputWithHTTPStatus); ok {
+		ht.SuccessStatus = outputWithStatus.HTTPStatus()
 	} else {
-		ht.SuccessStatus = http.StatusOK
+		if h.skipRendering && !h.outputWithWriter {
+			ht.SuccessStatus = http.StatusNoContent
+		} else {
+			ht.SuccessStatus = http.StatusOK
+		}
 	}
 }
 


Test patch:
diff --git a/openapi/collector_test.go b/openapi/collector_test.go
index 5ad9f89..35c052a 100644
--- a/openapi/collector_test.go
+++ b/openapi/collector_test.go
@@ -323,3 +323,79 @@ func TestCollector_Collect_CombineErrors(t *testing.T) {
 	  }
 	}`), collector.Reflector().SpecEns())
 }
+
+// Output that implements OutputWithHTTPStatus interface.
+type outputWithHTTPStatuses struct {
+	Number int `json:"number"`
+}
+
+func (outputWithHTTPStatuses) HTTPStatus() int {
+	return http.StatusCreated
+}
+
+func (outputWithHTTPStatuses) ExpectedHTTPStatuses() []int {
+	return []int{http.StatusCreated, http.StatusOK}
+}
+
+func TestCollector_Collect_multipleHttpStatuses(t *testing.T) {
+	c := openapi.Collector{}
+	u := usecase.IOInteractor{}
+	u.SetTitle("Title")
+	u.SetName("name")
+	u.Input = new(struct{})
+	u.Output = new(outputWithHTTPStatuses)
+
+	require.NoError(t, c.Collect(http.MethodPost, "/foo", u, rest.HandlerTrait{
+		ReqValidator: &jsonschema.Validator{},
+	}))
+
+	assertjson.EqualMarshal(t, []byte(`{
+	  "openapi": "3.0.3",
+	  "info": {
+		"title": "",
+		"version": ""
+	  },
+	  "paths": {
+		"/foo": {
+		  "post": {
+			"summary": "Title",
+			"operationId": "name",
+			"responses": {
+			  "200": {
+				"description": "OK",
+				"content": {
+				  "application/json": {
+					"schema": {
+					  "$ref": "#/components/schemas/OpenapiTestOutputWithHTTPStatuses"
+					}
+				  }
+				}
+			  },
+			  "201": {
+				"description": "Created",
+				"content": {
+				  "application/json": {
+					"schema": {
+					  "$ref": "#/components/schemas/OpenapiTestOutputWithHTTPStatuses"
+					}
+				  }
+				}
+			  }
+			}
+		  }
+		}
+	  },
+	  "components": {
+		"schemas": {
+		  "OpenapiTestOutputWithHTTPStatuses": {
+			"type": "object",
+			"properties": {
+			  "number": {
+				"type": "integer"
+			  }
+			}
+		  }
+		}
+	  }
+	}`), c.Reflector().SpecEns())
+}
diff --git a/response/encoder_test.go b/response/encoder_test.go
index 1870627..3035ddf 100644
--- a/response/encoder_test.go
+++ b/response/encoder_test.go
@@ -203,3 +203,30 @@ func TestEncoder_SetupOutput_nonPtr(t *testing.T) {
 	assert.Equal(t, "32", w.Header().Get("Content-Length"))
 	assert.Equal(t, `{"items":["one","two","three"]}`+"\n", w.Body.String())
 }
+
+// Output that implements OutputWithHTTPStatus interface.
+type outputWithHTTPStatuses struct {
+	Number int `json:"number"`
+}
+
+func (outputWithHTTPStatuses) HTTPStatus() int {
+	return http.StatusCreated
+}
+
+func (outputWithHTTPStatuses) ExpectedHTTPStatuses() []int {
+	return []int{http.StatusCreated, http.StatusOK}
+}
+
+func TestEncoder_SetupOutput_httpStatus(t *testing.T) {
+	e := response.Encoder{}
+	ht := rest.HandlerTrait{}
+	e.SetupOutput(outputWithHTTPStatuses{}, &ht)
+
+	r, err := http.NewRequest(http.MethodPost, "/", nil)
+	require.NoError(t, err)
+
+	w := httptest.NewRecorder()
+	output := e.MakeOutput(w, ht)
+	e.WriteSuccessfulResponse(w, r, output, ht)
+	assert.Equal(t, http.StatusCreated, w.Code)
+}