Skip to content

Commit b27ac1e

Browse files
committed
Merge branch 'v3' into grpc_codegen
2 parents fb93322 + 76109bb commit b27ac1e

File tree

5 files changed

+150
-0
lines changed

5 files changed

+150
-0
lines changed

codegen/service/templates/service.go.tpl

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
type Service interface {
44
{{- range .Methods }}
55
{{ comment .Description }}
6+
{{- if .SkipResponseBodyEncodeDecode }}
7+
{{ comment "\nIf body implements [io.WriterTo], that implementation will be used instead. Consider [goa.design/goa/v3/pkg.SkipResponseWriter] to adapt existing implementations." }}
8+
{{- end }}
69
{{- if .ViewedResult }}
710
{{- if not .ViewedResult.ViewName }}
811
{{ comment "The \"view\" return value must have one of the following views" }}

http/codegen/templates/server_handler_init.go.tpl

+16
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ func {{ .HandlerInit }}(
8484
{{- if .Method.SkipResponseBodyEncodeDecode }}
8585
o := res.(*{{ .ServicePkgName }}.{{ .Method.ResponseStruct }})
8686
defer o.Body.Close()
87+
if wt, ok := o.Body.(io.WriterTo); ok {
88+
n, err := wt.WriteTo(w)
89+
if err != nil {
90+
if n == 0 {
91+
if err := encodeError(ctx, w, err); err != nil {
92+
errhandler(ctx, w, err)
93+
}
94+
} else {
95+
if f, ok := w.(http.Flusher); ok {
96+
f.Flush()
97+
}
98+
panic(http.ErrAbortHandler) // too late to write an error
99+
}
100+
}
101+
return
102+
}
87103
// handle immediate read error like a returned error
88104
buf := bufio.NewReader(o.Body)
89105
if _, err := buf.Peek(1); err != nil && err != io.EOF {

http/codegen/testdata/handler_init_functions.go

+16
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,22 @@ func NewMethodSkipResponseBodyEncodeDecodeHandler(
271271
}
272272
o := res.(*serviceskipresponsebodyencodedecode.MethodSkipResponseBodyEncodeDecodeResponseData)
273273
defer o.Body.Close()
274+
if wt, ok := o.Body.(io.WriterTo); ok {
275+
n, err := wt.WriteTo(w)
276+
if err != nil {
277+
if n == 0 {
278+
if err := encodeError(ctx, w, err); err != nil {
279+
errhandler(ctx, w, err)
280+
}
281+
} else {
282+
if f, ok := w.(http.Flusher); ok {
283+
f.Flush()
284+
}
285+
panic(http.ErrAbortHandler) // too late to write an error
286+
}
287+
}
288+
return
289+
}
274290
// handle immediate read error like a returned error
275291
buf := bufio.NewReader(o.Body)
276292
if _, err := buf.Peek(1); err != nil && err != io.EOF {

pkg/skip_response_writer.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package goa
2+
3+
import (
4+
"io"
5+
"sync"
6+
"sync/atomic"
7+
)
8+
9+
// SkipResponseWriter converts an io.WriterTo into a io.ReadCloser.
10+
// The Read/Close methods this function returns will pipe the Write calls that wt makes, to implement a Reader that has the written bytes.
11+
// If Read is called Close must also be called to avoid leaking memory.
12+
// The returned value implements io.WriterTo as well, so the generated handler will call that instead of the Read method.
13+
//
14+
// Server handlers that use SkipResponseBodyEncodeDecode() io.ReadCloser as a return type.
15+
func SkipResponseWriter(wt io.WriterTo) io.ReadCloser {
16+
return &writerToReaderAdapter{WriterTo: wt}
17+
}
18+
19+
type writerToReaderAdapter struct {
20+
io.WriterTo
21+
prOnce sync.Once
22+
pr *io.PipeReader
23+
}
24+
25+
func (a *writerToReaderAdapter) initPipe() {
26+
r, w := io.Pipe()
27+
go func() {
28+
_, err := a.WriteTo(w)
29+
w.CloseWithError(err)
30+
}()
31+
a.pr = r
32+
}
33+
34+
func (a *writerToReaderAdapter) Read(b []byte) (n int, err error) {
35+
a.prOnce.Do(a.initPipe)
36+
return a.pr.Read(b)
37+
}
38+
39+
func (a *writerToReaderAdapter) Close() error {
40+
a.prOnce.Do(a.initPipe)
41+
return a.pr.Close()
42+
}
43+
44+
type writeCounter struct {
45+
io.Writer
46+
n atomic.Int64
47+
}
48+
49+
func (wc *writeCounter) Count() int64 { return wc.n.Load() }
50+
func (wc *writeCounter) Write(b []byte) (n int, err error) {
51+
n, err = wc.Writer.Write(b)
52+
wc.n.Add(int64(n))
53+
return
54+
}
55+
56+
// WriterToFunc impelments [io.WriterTo]. The io.Writer passed to the function will be wrapped.
57+
type WriterToFunc func(w io.Writer) (err error)
58+
59+
// WriteTo writes to w.
60+
//
61+
// The value in w is wrapped when passed to fn keeping track of how bytes are written by fn.
62+
func (fn WriterToFunc) WriteTo(w io.Writer) (n int64, err error) {
63+
wc := writeCounter{Writer: w}
64+
err = fn(&wc)
65+
return wc.Count(), err
66+
}

pkg/skip_response_writer_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package goa
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestSkipResponseWriter(t *testing.T) {
11+
const input = "Hello, World!"
12+
var responseWriter io.ReadCloser
13+
14+
responseWriter = SkipResponseWriter(strings.NewReader(input))
15+
defer func() {
16+
err := responseWriter.Close()
17+
if err != nil {
18+
t.Error(err)
19+
}
20+
}()
21+
_, ok := responseWriter.(io.WriterTo)
22+
if !ok {
23+
t.Errorf("SkipResponseWriter's result must implement io.WriterTo")
24+
}
25+
26+
var writerToBuffer bytes.Buffer
27+
_, err := io.Copy(&writerToBuffer, responseWriter) // io.Copy uses WriterTo if implemented
28+
if err != nil {
29+
t.Fatal(err)
30+
}
31+
if writerToBuffer.String() != input {
32+
t.Errorf("WriteTo: expected=%q actual=%q", input, writerToBuffer.String())
33+
}
34+
35+
responseWriter = SkipResponseWriter(strings.NewReader(input))
36+
defer func() {
37+
err := responseWriter.Close()
38+
if err != nil {
39+
t.Error(err)
40+
}
41+
}()
42+
readBytes, err := io.ReadAll(responseWriter) // io.ReadAll ignores WriterTo and calls Read
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
if string(readBytes) != input {
47+
t.Errorf("Read: expected=%q actual=%q", input, string(readBytes))
48+
}
49+
}

0 commit comments

Comments
 (0)