Repository: go-openapi/runtime
Pull request #129 (2019-02-28 11:49:34)

Add Support for CSV
Add csv support for consumers and producers.

codecov[bot]: # [Codecov](https://codecov.io/gh/go-openapi/runtime/pull/129?src=pr&el=h1) Report
> Merging [#129](https://codecov.io/gh/go-openapi/runtime/pull/129?src=pr&el=desc) into [master](https://codecov.io/gh/go-openapi/runtime/commit/41e24cc66d7af6af39eb9b5a6418e901bcdd333c?src=pr&el=desc) will **decrease** coverage by `0.04%`.
> The diff coverage is `76.92%`.

[![Impacted file tree graph](https://codecov.io/gh/go-openapi/runtime/pull/129/graphs/tree.svg?width=650&token=6wl4NsM8aQ&height=150&src=pr)](https://codecov.io/gh/go-openapi/runtime/pull/129?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #129      +/-   ##
==========================================
- Coverage   80.59%   80.54%   -0.05%     
==========================================
  Files          39       40       +1     
  Lines        2231     2257      +26     
==========================================
+ Hits         1798     1818      +20     
- Misses        341      344       +3     
- Partials       92       95       +3
```


| [Impacted Files](https://codecov.io/gh/go-openapi/runtime/pull/129?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [csv.go](https://codecov.io/gh/go-openapi/runtime/pull/129/diff?src=pr&el=tree#diff-Y3N2Lmdv) | `76.92% <76.92%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-openapi/runtime/pull/129?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/go-openapi/runtime/pull/129?src=pr&el=footer). Last update [41e24cc...d8fe45e](https://codecov.io/gh/go-openapi/runtime/pull/129?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/client/request.go b/client/request.go
index a1c1915..e643cf1 100644
--- a/client/request.go
+++ b/client/request.go
@@ -213,9 +213,6 @@ func (r *request) buildHTTP(mediaType, basePath string, producers map[string]run
 				return nil, err
 			}
 
-			if _, err := r.buf.Write(b.Bytes()); err != nil {
-				return nil, err
-			}
 			return ioutil.NopCloser(&b), nil
 		}
 
diff --git a/client/runtime.go b/client/runtime.go
index c222d94..50c4fd4 100644
--- a/client/runtime.go
+++ b/client/runtime.go
@@ -207,6 +207,7 @@ func New(host, basePath string, schemes []string) *Runtime {
 		runtime.XMLMime:     runtime.XMLConsumer(),
 		runtime.TextMime:    runtime.TextConsumer(),
 		runtime.HTMLMime:    runtime.TextConsumer(),
+		runtime.CSVMime:     runtime.CSVConsumer(),
 		runtime.DefaultMime: runtime.ByteStreamConsumer(),
 	}
 	rt.Producers = map[string]runtime.Producer{
@@ -214,6 +215,7 @@ func New(host, basePath string, schemes []string) *Runtime {
 		runtime.XMLMime:     runtime.XMLProducer(),
 		runtime.TextMime:    runtime.TextProducer(),
 		runtime.HTMLMime:    runtime.TextProducer(),
+		runtime.CSVMime:     runtime.CSVProducer(),
 		runtime.DefaultMime: runtime.ByteStreamProducer(),
 	}
 	rt.Transport = http.DefaultTransport
diff --git a/constants.go b/constants.go
index 30f36fa..a4de897 100644
--- a/constants.go
+++ b/constants.go
@@ -38,6 +38,8 @@ const (
 	TextMime = "text/plain"
 	// HTMLMime the html mime type
 	HTMLMime = "text/html"
+	// CSVMime the csv mime type
+	CSVMime = "text/csv"
 	// MultipartFormMime the multipart form mime type
 	MultipartFormMime = "multipart/form-data"
 	// URLencodedFormMime the url encoded form mime type
diff --git a/csv.go b/csv.go
new file mode 100644
index 0000000..d807bd9
--- /dev/null
+++ b/csv.go
@@ -0,0 +1,77 @@
+// Copyright 2015 go-swagger maintainers
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runtime
+
+import (
+	"bytes"
+	"encoding/csv"
+	"errors"
+	"io"
+)
+
+// CSVConsumer creates a new CSV consumer
+func CSVConsumer() Consumer {
+	return ConsumerFunc(func(reader io.Reader, data interface{}) error {
+		if reader == nil {
+			return errors.New("CSVConsumer requires a reader")
+		}
+
+		csvReader := csv.NewReader(reader)
+		writer, ok := data.(io.Writer)
+		if !ok {
+			return errors.New("data type must be io.Writer")
+		}
+		csvWriter := csv.NewWriter(writer)
+		records, err := csvReader.ReadAll()
+		if err != nil {
+			return err
+		}
+		for _, r := range records {
+			if err := csvWriter.Write(r); err != nil {
+				return err
+			}
+		}
+		csvWriter.Flush()
+		return nil
+	})
+}
+
+// CSVProducer creates a new CSV producer
+func CSVProducer() Producer {
+	return ProducerFunc(func(writer io.Writer, data interface{}) error {
+		if writer == nil {
+			return errors.New("CSVProducer requires a writer")
+		}
+
+		dataBytes, ok := data.([]byte)
+		if !ok {
+			return errors.New("data type must be byte array")
+		}
+
+		csvReader := csv.NewReader(bytes.NewBuffer(dataBytes))
+		records, err := csvReader.ReadAll()
+		if err != nil {
+			return err
+		}
+		csvWriter := csv.NewWriter(writer)
+		for _, r := range records {
+			if err := csvWriter.Write(r); err != nil {
+				return err
+			}
+		}
+		csvWriter.Flush()
+		return nil
+	})
+}


Test patch:
diff --git a/client/request_test.go b/client/request_test.go
index e6ee6ac..c1db584 100644
--- a/client/request_test.go
+++ b/client/request_test.go
@@ -18,9 +18,14 @@ import (
 	"bytes"
 	"encoding/json"
 	"encoding/xml"
+	"errors"
+	"io"
 	"io/ioutil"
 	"mime"
 	"mime/multipart"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
@@ -29,6 +34,7 @@ import (
 	"github.com/go-openapi/runtime"
 	"github.com/go-openapi/strfmt"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var testProducers = map[string]runtime.Producer{
@@ -509,3 +515,81 @@ func TestBuildRequest_BuildHTTP_EscapedPath(t *testing.T) {
 		assert.Equal(t, runtime.JSONMime, req.Header.Get(runtime.HeaderContentType))
 	}
 }
+
+type testReqFn func(*testing.T, *http.Request)
+
+type testRoundTripper struct {
+	tr          http.RoundTripper
+	testFn      testReqFn
+	testHarness *testing.T
+}
+
+func (t *testRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
+	t.testFn(t.testHarness, req)
+	return t.tr.RoundTrip(req)
+}
+
+func TestGetBodyCallsBeforeRoundTrip(t *testing.T) {
+
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		rw.WriteHeader(http.StatusCreated)
+		_, err := rw.Write([]byte("test result"))
+		require.NoError(t, err)
+	}))
+	defer server.Close()
+	hu, _ := url.Parse(server.URL)
+
+	client := http.DefaultClient
+	transport := http.DefaultTransport
+
+	client.Transport = &testRoundTripper{
+		tr:          transport,
+		testHarness: t,
+		testFn: func(t *testing.T, req *http.Request) {
+			// Read the body once before sending the request
+			body, err := req.GetBody()
+			require.NoError(t, err)
+			bodyContent, err := ioutil.ReadAll(io.Reader(body))
+			require.EqualValues(t, req.ContentLength, len(bodyContent))
+			require.NoError(t, err)
+			require.EqualValues(t, "\"test body\"\n", string(bodyContent))
+
+			// Read the body a second time before sending the request
+			body, err = req.GetBody()
+			require.NoError(t, err)
+			bodyContent, err = ioutil.ReadAll(io.Reader(body))
+			require.NoError(t, err)
+			require.EqualValues(t, req.ContentLength, len(bodyContent))
+			require.EqualValues(t, "\"test body\"\n", string(bodyContent))
+		},
+	}
+
+	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
+		return req.SetBodyParam("test body")
+	})
+
+	operation := &runtime.ClientOperation{
+		ID:          "getSites",
+		Method:      "POST",
+		PathPattern: "/",
+		Params:      rwrtr,
+		Client:      client,
+		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+			if response.Code() == http.StatusCreated {
+				var result string
+				if err := consumer.Consume(response.Body(), &result); err != nil {
+					return nil, err
+				}
+				return result, nil
+			}
+			return nil, errors.New("Unexpected error code")
+		}),
+	}
+
+	openAPIClient := New(hu.Host, "/", []string{"http"})
+	res, err := openAPIClient.Submit(operation)
+
+	require.NoError(t, err)
+	actual := res.(string)
+	require.EqualValues(t, "test result", actual)
+}
diff --git a/client/runtime_test.go b/client/runtime_test.go
index d1dccae..dfd6d7c 100644
--- a/client/runtime_test.go
+++ b/client/runtime_test.go
@@ -39,7 +39,6 @@ import (
 
 // task This describes a task. Tasks require a content property to be set.
 type task struct {
-
 	// Completed
 	Completed bool `json:"completed" xml:"completed"`
 
@@ -366,6 +365,50 @@ func TestRuntime_TextCanary(t *testing.T) {
 	}
 }
 
+func TestRuntime_CSVCanary(t *testing.T) {
+	// test that it can make a simple csv request
+	// and get the response for it.
+	result := `task,content,result
+1,task1,ok
+2,task2,fail
+`
+	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		rw.Header().Add(runtime.HeaderContentType, runtime.CSVMime)
+		rw.WriteHeader(http.StatusOK)
+		_, _ = rw.Write([]byte(result))
+	}))
+	defer server.Close()
+
+	rwrtr := runtime.ClientRequestWriterFunc(func(req runtime.ClientRequest, _ strfmt.Registry) error {
+		return nil
+	})
+
+	hu, _ := url.Parse(server.URL)
+	rt := New(hu.Host, "/", []string{"http"})
+	res, err := rt.Submit(&runtime.ClientOperation{
+		ID:          "getTasks",
+		Method:      "GET",
+		PathPattern: "/",
+		Params:      rwrtr,
+		Reader: runtime.ClientResponseReaderFunc(func(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+			if response.Code() == 200 {
+				var result bytes.Buffer
+				if err := consumer.Consume(response.Body(), &result); err != nil {
+					return nil, err
+				}
+				return result, nil
+			}
+			return nil, errors.New("Generic error")
+		}),
+	})
+
+	if assert.NoError(t, err) {
+		assert.IsType(t, bytes.Buffer{}, res)
+		actual := res.(bytes.Buffer)
+		assert.EqualValues(t, result, actual.String())
+	}
+}
+
 type roundTripperFunc func(*http.Request) (*http.Response, error)
 
 func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
diff --git a/csv_test.go b/csv_test.go
new file mode 100644
index 0000000..cfc4a93
--- /dev/null
+++ b/csv_test.go
@@ -0,0 +1,73 @@
+// Copyright 2015 go-swagger maintainers
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package runtime
+
+import (
+	"bytes"
+	"github.com/stretchr/testify/assert"
+	"io"
+	"net/http/httptest"
+	"testing"
+)
+
+const consProdCSV = `name,country,age
+John,US,19
+Mike,US,20
+`
+
+type csvEmptyReader struct{}
+
+func (r *csvEmptyReader) Read(d []byte) (int, error) {
+	return 0, io.EOF
+}
+
+func TestCSVConsumer(t *testing.T) {
+	cons := CSVConsumer()
+	reader := bytes.NewBuffer([]byte(consProdCSV))
+
+	outBuf := new(bytes.Buffer)
+	err := cons.Consume(reader, outBuf)
+	assert.NoError(t, err)
+	assert.Equal(t, consProdCSV, outBuf.String())
+
+	outBuf2 := new(bytes.Buffer)
+	err = cons.Consume(nil, outBuf2)
+	assert.Error(t, err)
+
+	err = cons.Consume(reader, struct{}{})
+	assert.Error(t, err)
+
+	emptyOutBuf := new(bytes.Buffer)
+	err = cons.Consume(&csvEmptyReader{}, emptyOutBuf)
+	assert.NoError(t, err)
+	assert.Equal(t, "", emptyOutBuf.String())
+}
+
+func TestCSVProducer(t *testing.T) {
+	prod := CSVProducer()
+	data := []byte(consProdCSV)
+
+	rw := httptest.NewRecorder()
+	err := prod.Produce(rw, data)
+	assert.NoError(t, err)
+	assert.Equal(t, consProdCSV, rw.Body.String())
+
+	rw2 := httptest.NewRecorder()
+	err = prod.Produce(rw2, struct{}{})
+	assert.Error(t, err)
+
+	err = prod.Produce(nil, data)
+	assert.Error(t, err)
+}