Repository: swaggest/rest
Pull request #16 (2020-12-29 21:23:29)

Allow custom Content-Type for stream writer

github-actions[bot]: ### Unit Test Coverage
total:	(statements)	81.5%
<details><summary>Coverage diff with base branch</summary>

```diff
115c115,116
< github.com/swaggest/rest/response/encoder.go	Write	90.0%
---
> github.com/swaggest/rest/response/encoder.go	setHeaders	88.9%
> github.com/swaggest/rest/response/encoder.go	Write	85.7%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=h1) Report
> Merging [#16](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=desc) (c7b1b45) into [master](https://codecov.io/gh/swaggest/rest/commit/8954b1c2c0d1ae28281cb437cbbe8fb31e33f2d6?el=desc) (8954b1c) will **decrease** coverage by `0.02%`.
> The diff coverage is `63.63%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/16/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn)](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #16      +/-   ##
==========================================
- Coverage   73.91%   73.88%   -0.03%     
==========================================
  Files          26       26              
  Lines        1457     1463       +6     
==========================================
+ Hits         1077     1081       +4     
- Misses        240      241       +1     
- Partials      140      141       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `73.88% <63.63%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [response/encoder.go](https://codecov.io/gh/swaggest/rest/pull/16/diff?src=pr&el=tree#diff-cmVzcG9uc2UvZW5jb2Rlci5nbw==) | `63.58% <63.63%> (+0.11%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=footer). Last update [8954b1c...c7b1b45](https://codecov.io/gh/swaggest/rest/pull/16?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

github-actions[bot]: ### Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    1.57µs ± 3%    1.94µs ± 3%  +23.99%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           852ns ± 2%    1026ns ± 2%  +20.45%  (p=0.008 n=5+5)
DecoderFunc_Decode-2                      2.35µs ± 2%    2.93µs ± 3%  +24.77%  (p=0.008 n=5+5)
Decoder_Decode_json-2                     28.1µs ± 2%    32.9µs ± 1%  +17.17%  (p=0.008 n=5+5)
Decoder_Decode_queryObject-2              6.30µs ± 3%    7.34µs ± 3%  +16.37%  (p=0.008 n=5+5)
Decoder_Decode_jsonParam-2                2.30µs ± 4%    2.65µs ± 2%  +15.20%  (p=0.008 n=5+5)
DecoderFactory_SetDecoderFunc-2           1.96µs ± 2%    2.32µs ± 2%  +18.73%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              13.7µs ± 3%    15.6µs ± 3%  +14.14%  (p=0.008 n=5+5)
Middleware_control-2                      3.68µs ± 5%    4.67µs ± 2%  +27.05%  (p=0.008 n=5+5)

name                                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    2.50kB ± 0%    2.50kB ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            448B ± 0%      448B ± 0%     ~     (all equal)
DecoderFunc_Decode-2                      1.51kB ± 0%    1.51kB ± 0%     ~     (all equal)
Decoder_Decode_json-2                     12.4kB ± 0%    12.4kB ± 0%     ~     (all equal)
Decoder_Decode_queryObject-2              2.00kB ± 0%    2.00kB ± 0%     ~     (all equal)
Decoder_Decode_jsonParam-2                  752B ± 0%      752B ± 0%     ~     (all equal)
DecoderFactory_SetDecoderFunc-2           1.02kB ± 0%    1.02kB ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              1.11kB ± 1%    1.11kB ± 2%     ~     (p=0.952 n=5+5)
Middleware_control-2                      11.2kB ± 0%    11.2kB ± 0%     ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2      8.00 ± 0%      8.00 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            4.00 ± 0%      4.00 ± 0%     ~     (all equal)
DecoderFunc_Decode-2                        12.0 ± 0%      12.0 ± 0%     ~     (all equal)
Decoder_Decode_json-2                        169 ± 0%       169 ± 0%     ~     (all equal)
Decoder_Decode_queryObject-2                36.0 ± 0%      36.0 ± 0%     ~     (all equal)
Decoder_Decode_jsonParam-2                  12.0 ± 0%      12.0 ± 0%     ~     (all equal)
DecoderFactory_SetDecoderFunc-2             16.0 ± 0%      16.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                                11.0 ± 0%      11.0 ± 0%     ~     (all equal)
Middleware_control-2                        9.00 ± 0%      9.00 ± 0%     ~     (all equal)
```
</details>

### Examples Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             24.9µs ± 2%    29.1µs ± 3%  +16.70%  (p=0.008 n=5+5)
_directGzipHead-2         24.8µs ± 1%    29.2µs ± 1%  +17.63%  (p=0.008 n=5+5)
_noDirectGzip-2            138µs ± 3%     156µs ± 3%  +12.92%  (p=0.008 n=5+5)
_directGzip_decode-2       528µs ± 2%     615µs ± 4%  +16.50%  (p=0.008 n=5+5)
_noDirectGzip_decode-2     137µs ± 4%     155µs ± 3%  +13.44%  (p=0.008 n=5+5)
_jsonBody-2               29.3µs ± 4%    36.8µs ± 3%  +25.35%  (p=0.008 n=5+5)
_jsonBodyValidation-2     52.4µs ± 7%    61.5µs ± 3%  +17.35%  (p=0.008 n=5+5)
_outputHeaders-2          25.4µs ± 1%    27.7µs ± 5%   +8.93%  (p=0.008 n=5+5)
_validation-2             47.1µs ± 7%    58.5µs ± 1%  +24.10%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            29.6µs ± 3%    36.1µs ± 5%  +21.89%  (p=0.008 n=5+5)
_ok-2                     31.9µs ± 4%    37.6µs ± 3%  +17.65%  (p=0.008 n=5+5)
_invalidBody-2            44.4µs ± 6%    51.8µs ± 4%  +16.69%  (p=0.008 n=5+5)

name                    old B:rcvd/op  new B:rcvd/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                639 ± 0%       639 ± 0%     ~     (all equal)
_directGzipHead-2            183 ± 0%       183 ± 0%     ~     (all equal)
_noDirectGzip-2            1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_directGzip_decode-2         639 ± 0%       639 ± 0%     ~     (all equal)
_noDirectGzip_decode-2     1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_jsonBody-2                  194 ± 0%       194 ± 0%     ~     (all equal)
_jsonBodyValidation-2        194 ± 0%       194 ± 0%     ~     (all equal)
_outputHeaders-2             155 ± 0%       155 ± 0%     ~     (all equal)
_validation-2                177 ± 0%       177 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               337 ± 0%       337 ± 0%     ~     (all equal)
_ok-2                        359 ± 0%       359 ± 0%     ~     (p=0.444 n=5+5)
_invalidBody-2               435 ± 0%       435 ± 0%     ~     (all equal)

name                    old B:sent/op  new B:sent/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-2            104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-2              117 ± 0%       117 ± 0%     ~     (all equal)
_directGzip_decode-2         116 ± 0%       116 ± 0%     ~     (all equal)
_noDirectGzip_decode-2       130 ± 0%       130 ± 0%     ~     (all equal)
_jsonBody-2                  181 ± 0%       181 ± 0%     ~     (all equal)
_jsonBodyValidation-2        192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-2            77.0 ± 0%      77.0 ± 0%     ~     (all equal)
_validation-2                170 ± 0%       170 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_ok-2                       74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_invalidBody-2               137 ± 0%       137 ± 0%     ~     (all equal)

name                    old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2              40.1k ± 2%     34.3k ± 3%  -14.39%  (p=0.008 n=5+5)
_directGzipHead-2          40.3k ± 1%     34.2k ± 1%  -14.99%  (p=0.008 n=5+5)
_noDirectGzip-2            7.24k ± 3%     6.41k ± 3%  -11.44%  (p=0.008 n=5+5)
_directGzip_decode-2       1.89k ± 2%     1.63k ± 4%  -14.12%  (p=0.008 n=5+5)
_noDirectGzip_decode-2     7.31k ± 4%     6.44k ± 3%  -11.89%  (p=0.008 n=5+5)
_jsonBody-2                34.1k ± 4%     27.2k ± 3%  -20.25%  (p=0.008 n=5+5)
_jsonBodyValidation-2      19.1k ± 8%     16.3k ± 3%  -14.92%  (p=0.008 n=5+5)
_outputHeaders-2           39.3k ± 1%     36.1k ± 5%   -8.12%  (p=0.008 n=5+5)
_validation-2              21.2k ± 6%     17.1k ± 1%  -19.55%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2             33.8k ± 3%     27.7k ± 5%  -17.92%  (p=0.008 n=5+5)
_ok-2                      31.3k ± 4%     26.6k ± 3%  -15.06%  (p=0.008 n=5+5)
_invalidBody-2             22.5k ± 6%     19.3k ± 4%  -14.30%  (p=0.008 n=5+5)

name                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             3.86kB ± 0%    3.86kB ± 0%     ~     (p=0.135 n=5+5)
_directGzipHead-2         3.86kB ± 0%    3.86kB ± 0%   +0.10%  (p=0.008 n=5+5)
_noDirectGzip-2           6.09kB ±20%    6.21kB ±19%     ~     (p=0.841 n=5+5)
_directGzip_decode-2       403kB ± 0%     403kB ± 0%     ~     (p=0.310 n=5+5)
_noDirectGzip_decode-2    5.32kB ± 7%    5.11kB ± 7%     ~     (p=0.310 n=5+5)
_jsonBody-2               5.98kB ± 0%    5.98kB ± 0%   +0.10%  (p=0.008 n=5+5)
_jsonBodyValidation-2     18.2kB ± 0%    18.2kB ± 0%   +0.06%  (p=0.008 n=5+5)
_outputHeaders-2          3.64kB ± 0%    3.64kB ± 0%     ~     (p=0.238 n=5+4)
_validation-2             16.2kB ± 0%    16.2kB ± 0%     ~     (p=0.349 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            5.65kB ± 0%    5.65kB ± 0%   +0.06%  (p=0.024 n=5+5)
_ok-2                     5.56kB ± 0%    5.56kB ± 0%     ~     (p=0.171 n=4+4)
_invalidBody-2            9.47kB ± 0%    9.47kB ± 0%     ~     (p=0.314 n=4+4)

name                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2               40.0 ± 0%      40.0 ± 0%     ~     (all equal)
_directGzipHead-2           40.0 ± 0%      40.0 ± 0%     ~     (all equal)
_noDirectGzip-2             47.6 ± 3%      47.8 ± 4%     ~     (p=1.000 n=5+5)
_directGzip_decode-2         497 ± 0%       497 ± 0%     ~     (p=0.444 n=5+5)
_noDirectGzip_decode-2      48.0 ± 0%      48.0 ± 0%     ~     (all equal)
_jsonBody-2                 57.0 ± 0%      57.0 ± 0%     ~     (all equal)
_jsonBodyValidation-2        144 ± 0%       144 ± 0%     ~     (all equal)
_outputHeaders-2            35.0 ± 0%      35.0 ± 0%     ~     (all equal)
_validation-2                131 ± 0%       131 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              63.0 ± 0%      63.0 ± 0%     ~     (all equal)
_ok-2                       59.0 ± 0%      59.0 ± 0%     ~     (all equal)
_invalidBody-2               109 ± 0%       109 ± 0%     ~     (all equal)
```
</details>
<!-- Sticky Pull Request Commentbench -->

Patch:
diff --git a/response/encoder.go b/response/encoder.go
index 6888464..43cd73f 100644
--- a/response/encoder.go
+++ b/response/encoder.go
@@ -292,7 +292,7 @@ func (h *Encoder) MakeOutput(w http.ResponseWriter, ht rest.HandlerTrait) interf
 
 	if h.outputWithWriter {
 		if withWriter, ok := output.(usecase.OutputWithWriter); ok {
-			if h.outputHeadersEncoder != nil {
+			if h.outputHeadersEncoder != nil || ht.SuccessContentType != "" {
 				withWriter.SetWriter(&writerWithHeaders{
 					ResponseWriter: w,
 					responseWriter: h,
@@ -317,15 +317,29 @@ type writerWithHeaders struct {
 	headersSet     bool
 }
 
+func (w *writerWithHeaders) setHeaders() error {
+	if w.responseWriter.outputHeadersEncoder == nil {
+		return nil
+	}
+
+	headers, err := w.responseWriter.outputHeadersEncoder.Encode(w.output)
+	if err != nil {
+		return err
+	}
+
+	for header, val := range headers {
+		if len(val) == 1 {
+			w.Header().Set(header, val[0])
+		}
+	}
+
+	return err
+}
+
 func (w *writerWithHeaders) Write(data []byte) (int, error) {
 	if !w.headersSet {
-		headers, err := w.responseWriter.outputHeadersEncoder.Encode(w.output)
-		if err == nil {
-			for header, val := range headers {
-				if len(val) == 1 {
-					w.Header().Set(header, val[0])
-				}
-			}
+		if err := w.setHeaders(); err != nil {
+			return 0, err
 		}
 
 		if w.trait.SuccessContentType != "" {


Test patch:
diff --git a/response/encoder_test.go b/response/encoder_test.go
index 8386d3b..dfef550 100644
--- a/response/encoder_test.go
+++ b/response/encoder_test.go
@@ -79,7 +79,9 @@ func TestEncoder_SetupOutput(t *testing.T) {
 func TestEncoder_SetupOutput_withWriter(t *testing.T) {
 	e := response.Encoder{}
 
-	ht := rest.HandlerTrait{}
+	ht := rest.HandlerTrait{
+		SuccessContentType: "application/x-vnd-foo",
+	}
 
 	type outputPort struct {
 		Name string `header:"X-Name" json:"-"`
@@ -104,6 +106,38 @@ func TestEncoder_SetupOutput_withWriter(t *testing.T) {
 
 	e.WriteSuccessfulResponse(w, r, output, ht)
 	assert.Equal(t, http.StatusOK, w.Code)
+	assert.Equal(t, "application/x-vnd-foo", w.Header().Get("Content-Type"))
 	assert.Equal(t, "1,2,3", w.Body.String())
 	assert.Equal(t, "Jane", w.Header().Get("X-Name"))
 }
+
+func TestEncoder_SetupOutput_withWriterContentType(t *testing.T) {
+	e := response.Encoder{}
+
+	ht := rest.HandlerTrait{
+		SuccessContentType: "application/x-vnd-foo",
+	}
+
+	type outputPort struct {
+		usecase.OutputWithEmbeddedWriter
+	}
+
+	e.SetupOutput(new(outputPort), &ht)
+
+	w := httptest.NewRecorder()
+	r, err := http.NewRequest(http.MethodGet, "/", nil)
+	require.NoError(t, err)
+
+	output := e.MakeOutput(w, ht)
+
+	out, ok := output.(*outputPort)
+	assert.True(t, ok)
+
+	_, err = out.Write([]byte("1,2,3"))
+	require.NoError(t, err)
+
+	e.WriteSuccessfulResponse(w, r, output, ht)
+	assert.Equal(t, http.StatusOK, w.Code)
+	assert.Equal(t, "application/x-vnd-foo", w.Header().Get("Content-Type"))
+	assert.Equal(t, "1,2,3", w.Body.String())
+}