Skip to content

Commit

Permalink
Add license (#3)
Browse files Browse the repository at this point in the history
Also updated unit tests to use t.Run() and updated some documentation.
  • Loading branch information
lag13 authored Oct 24, 2017
1 parent 898186e commit 34f1181
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 99 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Lucas Groenendaal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# httpparse [![Build Status](https://travis-ci.org/lag13/httpparse.svg?branch=master)](https://travis-ci.org/lag13/httpparse) [![GoDoc](https://godoc.org/github.com/lag13/httpparse?status.svg)](http://godoc.org/github.com/lag13/httpparse) [![Go Report Card](https://goreportcard.com/badge/github.com/lag13/httpparse)](https://goreportcard.com/report/github.com/lag13/httpparse)
Provides components to help parse http responses. See
http://godoc.org/github.com/lag13/httpparse for documentation.
2 changes: 0 additions & 2 deletions README.org

This file was deleted.

43 changes: 43 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package httpparse_test

import (
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/lag13/httpparse"
)

func ExampleRawBody() {
resp := &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"field1":"hello there", "field2":42}`)),
}
body, err := httpparse.RawBody(resp, []int{http.StatusOK})
if err != nil {
fmt.Println("got error:", err)
}
fmt.Printf("got body: %s\n", body)

// Output: got body: {"field1":"hello there", "field2":42}
}

func ExampleJSON() {
var structuredBody struct {
Field1 string `json:"field1"`
Field2 int `json:"field2"`
}
resp := &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"field1":"hello there", "field2":42}`)),
}
if err := httpparse.JSON(resp, http.StatusOK, &structuredBody); err != nil {
fmt.Println("got error:", err)
}
fmt.Println("field1 is:", structuredBody.Field1)
fmt.Println("field2 is:", structuredBody.Field2)

// Output: field1 is: hello there
// field2 is: 42
}
36 changes: 22 additions & 14 deletions httpparse.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
// Package httpparse contains utilities for parsing http responses.
/*
Package httpparse provides utilities for parsing http responses.
Motivation
Parsing a http response into some sort of data structure (or even just
pulling out the raw body) follows a very repetitive pattern of:
1. defer'ring closing the response body.
2. checking that we got the expected status code.
3. reading and parsing the response body.
4. generating error messages if any of these things fail.
It's not much logic but I've repeated it enough to want an abstraction
and even if this package is not used it serves as a nice reference.
*/
package httpparse

import (
Expand Down Expand Up @@ -73,26 +90,17 @@ func RawBody(resp *http.Response, wantStatuses []int, readLimit ...int64) (body

// JSON parses a http response who's body contains JSON and closes the
// response body. Most of the logic revolves around trying to produce
// helpful error messages. For example, if the response status code is
// unexpected then you can't possibly know how to unmarshal that
// response body so we try to read part of the response body and
// return that as part of the error to provide context. But reading
// the response can fail, or we might not read ALL of the response,
// etc... and we just want a nice clear error message for all those
// cases.
func JSON(resp *http.Response, wantStatuses []int, v interface{}) error {
// clear error messages when edge cases are hit.
func JSON(resp *http.Response, wantStatus int, v interface{}) error {
defer resp.Body.Close()
if got, wants := resp.StatusCode, wantStatuses; !contains(wants, got) {
if got, want := resp.StatusCode, wantStatus; got != want {
maxBytes := int64(1 << 20)
limitedReader := &io.LimitedReader{
R: resp.Body,
N: maxBytes + 1,
}
err := fmt.Errorf("got status code %d but wanted %d", got, want)
body, readErr := ioutil.ReadAll(limitedReader)
err := fmt.Errorf("got status code %d but wanted one of %v", got, wants)
if len(wants) == 1 {
err = fmt.Errorf("got status code %d but wanted %d", got, wants[0])
}
if readErr != nil {
return fmt.Errorf("%v, also an error occurred when reading the response body: %v", err, readErr)
}
Expand Down
149 changes: 66 additions & 83 deletions httpparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ func (e errReadCloser) Close() error {
// the body and returns any expected errors.
func TestGetRawBody(t *testing.T) {
tests := []struct {
testScenario string
name string
resp *http.Response
expectStatuses []int
readLimit int64
wantBody string
wantErr string
}{
{
testScenario: "there was an error when reading the response body",
name: "error reading response body",
resp: &http.Response{
Body: errReadCloser{readErr: errors.New("some read err")},
},
Expand All @@ -52,7 +52,7 @@ func TestGetRawBody(t *testing.T) {
wantErr: "reading response body: some read err",
},
{
testScenario: "the response status code was unexpected",
name: "unexpected response status code",
resp: &http.Response{
StatusCode: 999,
Body: ioutil.NopCloser(strings.NewReader("woa there")),
Expand All @@ -63,7 +63,7 @@ func TestGetRawBody(t *testing.T) {
wantErr: "got status code 999 but wanted 200, body: woa there",
},
{
testScenario: "the response status code was unexpected (when expecting multiple status codes)",
name: "unexpected status code when expecting multiple status codes",
resp: &http.Response{
StatusCode: 999,
Body: ioutil.NopCloser(strings.NewReader("woa there")),
Expand All @@ -74,7 +74,7 @@ func TestGetRawBody(t *testing.T) {
wantErr: "got status code 999 but wanted one of [200 888], body: woa there",
},
{
testScenario: "the response body exceeded the limit so an error is returned",
name: "response body exceeded the limit",
resp: &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(strings.NewReader("a reeaaaallllly loooooooong responnnnnnssssseeeeee bodyyyyyyyy")),
Expand All @@ -85,7 +85,7 @@ func TestGetRawBody(t *testing.T) {
wantErr: "ioutil.ReadAll() is used to read the response body and we limit how much it can read because nothing is infinite. The response body contained more than the limit of 19 bytes. Either increase the limit or parse the response body another way",
},
{
testScenario: "the raw response body is returned",
name: "returned raw response body",
resp: &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(strings.NewReader("hello there buddy")),
Expand All @@ -96,28 +96,25 @@ func TestGetRawBody(t *testing.T) {
wantErr: "",
},
}
for i, test := range tests {
errorMsg := func(str string, args ...interface{}) {
t.Helper()
t.Errorf("Running test %d, where %s:\n"+str, append([]interface{}{i, test.testScenario}, args...)...)
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var body []byte
var err error
if test.readLimit == 0 {
body, err = httpparse.RawBody(test.resp, test.expectStatuses)
} else {
body, err = httpparse.RawBody(test.resp, test.expectStatuses, test.readLimit)
}

var body []byte
var err error
if test.readLimit == 0 {
body, err = httpparse.RawBody(test.resp, test.expectStatuses)
} else {
body, err = httpparse.RawBody(test.resp, test.expectStatuses, test.readLimit)
}

if test.wantErr == "" && err != nil {
errorMsg("got a non-nil error: %v", err)
} else if got, want := fmt.Sprintf("%v", err), test.wantErr; want != "" && !strings.Contains(got, want) {
errorMsg("got error message: %s, wanted message to contain the string: %s", got, want)
}
if got, want := string(body), test.wantBody; got != want {
errorMsg("got body\n %s\nwanted\n %s", got, want)
}
if test.wantErr == "" && err != nil {
t.Errorf("got a non-nil error: %v", err)
} else if got, want := fmt.Sprintf("%v", err), test.wantErr; want != "" && !strings.Contains(got, want) {
t.Errorf("got error message: %s, wanted message to contain the string: %s", got, want)
}
if got, want := string(body), test.wantBody; got != want {
t.Errorf("got body\n %s\nwanted\n %s", got, want)
}
})
}
}

Expand All @@ -131,99 +128,85 @@ type structuredJSON struct {
// unmarshals the raw JSON into the provided value.
func TestParseJSONResponse(t *testing.T) {
tests := []struct {
testScenario string
resp *http.Response
expectStatuses []int
readLimit int64
wantData structuredJSON
wantErr string
name string
resp *http.Response
expectStatus int
readLimit int64
wantData structuredJSON
wantErr string
}{
{
testScenario: "the response status code was unexpected",
name: "unexpected response status code",
resp: &http.Response{
StatusCode: 999,
Body: ioutil.NopCloser(strings.NewReader("woa there")),
},
expectStatuses: []int{200},
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200",
expectStatus: 200,
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200, body: woa there",
},
{
testScenario: "the response status code was unexpected (when expecting multiple status codes)",
resp: &http.Response{
StatusCode: 999,
Body: ioutil.NopCloser(strings.NewReader("woa there")),
},
expectStatuses: []int{200, 888},
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted one of [200 888], body: woa there",
},
{
testScenario: "the response status code was unexpected and there was an error when reading the response body",
name: "unexpected response status code and error reading response body",
resp: &http.Response{
StatusCode: 999,
Body: errReadCloser{readErr: errors.New("some read err")},
},
expectStatuses: []int{200},
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200, also an error occurred when reading the response body: some read err",
expectStatus: 200,
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200, also an error occurred when reading the response body: some read err",
},
{
testScenario: "the response status code was unexpected and there was more response body that we didn't read",
name: "unexpected response status code did not read all of response body",
resp: &http.Response{
StatusCode: 999,
Body: ioutil.NopCloser(strings.NewReader(strings.Repeat("z", 1<<20+1))),
},
expectStatuses: []int{200},
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200, the first 1048576 bytes of the response body are: zzz",
expectStatus: 200,
readLimit: 0,
wantData: structuredJSON{},
wantErr: "got status code 999 but wanted 200, the first 1048576 bytes of the response body are: zzz",
},
{
testScenario: "we get an error when unmarshalling the data",
name: "error when unmarshalling response body",
resp: &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(strings.NewReader(`lats`)),
},
expectStatuses: []int{400},
readLimit: 0,
wantData: structuredJSON{},
wantErr: "unmarshalling response body: invalid character 'l'",
expectStatus: 400,
readLimit: 0,
wantData: structuredJSON{},
wantErr: "unmarshalling response body: invalid character 'l'",
},
{
testScenario: "we get the structured data and no error",
name: "got the structured data",
resp: &http.Response{
StatusCode: 400,
Body: ioutil.NopCloser(strings.NewReader(`{"value_one":"hello there", "value_two":42}`)),
},
expectStatuses: []int{400},
readLimit: 0,
expectStatus: 400,
readLimit: 0,
wantData: structuredJSON{
ValueOne: "hello there",
ValueTwo: 42,
},
wantErr: "",
},
}
for i, test := range tests {
errorMsg := func(str string, args ...interface{}) {
t.Helper()
t.Errorf("Running test %d, where %s:\n"+str, append([]interface{}{i, test.testScenario}, args...)...)
}

var data structuredJSON
err := httpparse.JSON(test.resp, test.expectStatuses, &data)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var data structuredJSON
err := httpparse.JSON(test.resp, test.expectStatus, &data)

if test.wantErr == "" && err != nil {
errorMsg("got a non-nil error: %v", err)
} else if got, want := fmt.Sprintf("%v", err), test.wantErr; want != "" && !strings.Contains(got, want) {
errorMsg("got error message: %s, wanted message to contain the string: %s", got, want)
}
if got, want := data, test.wantData; got != want {
errorMsg("got data %+v, wanted %+v", got, want)
}
if test.wantErr == "" && err != nil {
t.Errorf("got a non-nil error: %v", err)
} else if got, want := fmt.Sprintf("%v", err), test.wantErr; want != "" && !strings.Contains(got, want) {
t.Errorf("got error message: %s, wanted message to contain the string: %s", got, want)
}
if got, want := data, test.wantData; got != want {
t.Errorf("got data %+v, wanted %+v", got, want)
}
})
}
}

0 comments on commit 34f1181

Please sign in to comment.