Skip to content

Commit

Permalink
feat: allow JSON response body to be streamed
Browse files Browse the repository at this point in the history
  • Loading branch information
padamstx committed Dec 4, 2019
1 parent a0df7be commit d1345d7
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 23 deletions.
6 changes: 4 additions & 2 deletions core/base_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -269,10 +270,11 @@ func (service *BaseService) Request(req *http.Request, result interface{}) (deta
return
}

// Operation was successful and we are expecting a response, so de-serialize the response.
// Operation was successful and we are expecting a response, so process the response.
if result != nil {
// For a JSON response, decode it into the response object.
if IsJSONMimeType(contentType) {
resultType := reflect.TypeOf(result).String()
if IsJSONMimeType(contentType) && resultType != "*io.ReadCloser" {

// First, read the response body into a byte array.
defer httpResponse.Body.Close()
Expand Down
115 changes: 100 additions & 15 deletions core/base_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ package core
// limitations under the License.

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -44,8 +46,6 @@ func TestGoodResponseJSON(t *testing.T) {
builder := NewRequestBuilder("POST")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

authenticator, err := NewBasicAuthenticator("xxx", "yyy")
Expand Down Expand Up @@ -73,6 +73,56 @@ func TestGoodResponseJSON(t *testing.T) {
assert.Equal(t, "wonder woman", *(result.Name))
}

// Test a JSON-based response that should be returned as a stream (io.ReadCloser).
func TestGoodResponseJSONStream(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, `{"name": "wonder woman"}`)
}))
defer server.Close()

builder := NewRequestBuilder("POST")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
req, _ := builder.Build()

authenticator, err := NewBasicAuthenticator("xxx", "yyy")
assert.Nil(t, err)
assert.NotNil(t, authenticator)

options := &ServiceOptions{
URL: server.URL,
Authenticator: authenticator,
}
service, err := NewBaseService(options)
assert.Nil(t, err)
assert.NotNil(t, service.Options.Authenticator)
assert.Equal(t, AUTHTYPE_BASIC, service.Options.Authenticator.AuthenticationType())

detailedResponse, err := service.Request(req, new(io.ReadCloser))
assert.Nil(t, err)
assert.NotNil(t, detailedResponse)
assert.Equal(t, http.StatusCreated, detailedResponse.StatusCode)
assert.Equal(t, "application/json", detailedResponse.Headers.Get("Content-Type"))

result, ok := detailedResponse.Result.(io.ReadCloser)
assert.Equal(t, true, ok)
assert.NotNil(t, result)

// Read the bytes from the response body and decode as JSON to verify.
responseBytes, err := ioutil.ReadAll(result)
assert.Nil(t, err)
assert.NotNil(t, responseBytes)

// Decode the byte array as JSON.
responseObj := new(Foo)
err = json.NewDecoder(bytes.NewReader(responseBytes)).Decode(&responseObj)
assert.Nil(t, err)
assert.NotNil(t, responseObj)
assert.Equal(t, "wonder woman", *(responseObj.Name))
}

// Verify that extra fields in result are silently ignored.
func TestGoodResponseJSONExtraFields(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -85,8 +135,6 @@ func TestGoodResponseJSONExtraFields(t *testing.T) {
builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

options := &ServiceOptions{
Expand All @@ -101,9 +149,9 @@ func TestGoodResponseJSONExtraFields(t *testing.T) {
assert.Equal(t, "wonder woman", *result.Name)
}

// Test a non-JSON response.
func TestGoodResponseNonJSON(t *testing.T) {
expectedResponse := []byte("This is a non-json response.")
// Test a binary response.
func TestGoodResponseStream(t *testing.T) {
expectedResponse := []byte("This is an octet stream response.")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/octet-stream")
_, _ = w.Write(expectedResponse)
Expand All @@ -113,8 +161,6 @@ func TestGoodResponseNonJSON(t *testing.T) {
builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

authenticator := &NoAuthAuthenticator{}
Expand Down Expand Up @@ -143,6 +189,51 @@ func TestGoodResponseNonJSON(t *testing.T) {
assert.Equal(t, expectedResponse, actualResponse)
}

// Test a text response.
func TestGoodResponseText(t *testing.T) {
expectedResponse := "This is a text response."
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "text/plain")
fmt.Fprint(w, expectedResponse)
}))
defer server.Close()

builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
req, _ := builder.Build()

authenticator := &NoAuthAuthenticator{}

options := &ServiceOptions{
URL: server.URL,
Authenticator: authenticator,
}
service, err := NewBaseService(options)
assert.Nil(t, err)
assert.NotNil(t, service.Options.Authenticator)
assert.Equal(t, AUTHTYPE_NOAUTH, service.Options.Authenticator.AuthenticationType())
detailedResponse, err := service.Request(req, new(string))
assert.Nil(t, err)
assert.NotNil(t, detailedResponse)
assert.Equal(t, "text/plain", detailedResponse.GetHeaders().Get("Content-Type"))
assert.Equal(t, http.StatusOK, detailedResponse.GetStatusCode())
assert.NotNil(t, detailedResponse.Result)
stream, ok := detailedResponse.Result.(io.ReadCloser)
assert.Equal(t, true, ok)
assert.NotNil(t, stream)

// Read the bytes from the returned stream and verify.
// This is a simulation of what the generated code will need to do once the stream is returned
// by the Request method.
defer stream.Close()
responseBytes, err := ioutil.ReadAll(stream)
assert.Nil(t, err)
assert.NotNil(t, responseBytes)
actualResponse := string(responseBytes)
assert.Equal(t, expectedResponse, actualResponse)
}

// Test a non-JSON response with no Content-Type set.
func TestGoodResponseNonJSONNoContentType(t *testing.T) {
expectedResponse := []byte("This is a non-json response.")
Expand All @@ -155,8 +246,6 @@ func TestGoodResponseNonJSONNoContentType(t *testing.T) {
builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

authenticator := &NoAuthAuthenticator{}
Expand Down Expand Up @@ -196,8 +285,6 @@ func TestGoodResponseJSONDeserFailure(t *testing.T) {
builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

options := &ServiceOptions{
Expand Down Expand Up @@ -226,8 +313,6 @@ func TestGoodResponseNoBody(t *testing.T) {
builder := NewRequestBuilder("GET")
_, err := builder.ConstructHTTPURL(server.URL, nil, nil)
assert.Nil(t, err)
builder.AddHeader("Content-Type", "Application/json").
AddQuery("Version", "2018-22-09")
req, _ := builder.Build()

authenticator, err := NewBasicAuthenticator("xxx", "yyy")
Expand Down
15 changes: 9 additions & 6 deletions core/detailed_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@ type DetailedResponse struct {
//
// If the operation was successful and the response body contains a JSON response, it is un-marshalled
// into an object of the appropriate type (defined by the particular operation), and the Result field will contain
// this response object. To retrieve this response object in its properly-typed form, use the
// generated service's "Get<operation-name>Result()" method.
// If there was an error while un-marshalling the JSON response body, then the RawResult field
// this response object. If there was an error while un-marshalling the JSON response body, then the RawResult field
// will be set to the byte array containing the response body.
//
// Alternatively, if the generated SDK code passes in a result object which is an io.ReadCloser instance,
// the JSON un-marshalling step is bypassed and the response body is simply returned in the Result field.
// This scenario would occur in a situation where the SDK would like to provide a streaming model for large JSON
// objects.
//
// If the operation was successful and the response body contains a non-JSON response,
// the Result field will be an instance of io.ReadCloser that can be used by the application to read
// the response data.
// the Result field will be an instance of io.ReadCloser that can be used by generated SDK code
// (or the application) to read the response data.
//
// If the operation was unsuccessful and the response body contains a JSON response,
// If the operation was unsuccessful and the response body contains a JSON error response,
// this field will contain an instance of map[string]interface{} which is the result of un-marshalling the
// response body as a "generic" JSON object.
// If the JSON response for an unsuccessful operation could not be properly un-marshalled, then the
Expand Down

0 comments on commit d1345d7

Please sign in to comment.