diff options
| -rw-r--r-- | README | 9 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 9 | ||||
| -rw-r--r-- | jttp.go | 34 | ||||
| -rw-r--r-- | jttp_test.go | 84 |
5 files changed, 146 insertions, 0 deletions
@@ -0,0 +1,9 @@ +JSON Over HTTP Client Go Library + +Jttp is a Go library that encapsulates the boilerplate of making a +HTTP request, checking the response status, and decoding the JSON in +the response body. + +The name jttp stands for JSON Text Transfer Protocol. It is a play on +HTTP, the HyerText Transfer Protocol. Although HTTP is often used to +transport JSON, JSON itself is not hypertext. @@ -0,0 +1,10 @@ +module git.samanthony.xyz/jttp + +go 1.25.9 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -0,0 +1,34 @@ +package jttp + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type Client struct { + c *http.Client +} + +func NewClient(c *http.Client) *Client { + return &Client{c} +} + +// Do sends an HTTP request, checks that the response status code matches +// wantStatus, and decodes the JSON response body into resp. +// +// An error is returned if the HTTP request fails, the response status +// code does not equal wantStatus, or the JSON decoding fails. +func (c *Client) Do(req *http.Request, wantStatus int, resp any) error { + hresp, err := c.c.Do(req) + if err != nil { + return err + } + defer hresp.Body.Close() + if hresp.StatusCode != wantStatus { + return fmt.Errorf("jttp: %s %s: %s (expected %d)", + req.Method, req.URL, hresp.Status, wantStatus) + } + dec := json.NewDecoder(hresp.Body) + return dec.Decode(resp) +} diff --git a/jttp_test.go b/jttp_test.go new file mode 100644 index 0000000..66256c0 --- /dev/null +++ b/jttp_test.go @@ -0,0 +1,84 @@ +package jttp_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "git.samanthony.xyz/jttp" +) + +type Person struct { + Name string + Age int +} + +func TestDo(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"name": "Alice", "age": 123}`) + })) + defer srv.Close() + + clnt := jttp.NewClient(srv.Client()) + url, err := url.Parse(srv.URL) + require.NoError(t, err) + req := &http.Request{Method: http.MethodGet, URL: url} + var alice Person + err = clnt.Do(req, http.StatusOK, &alice) + require.NoError(t, err) + require.Equal(t, Person{"Alice", 123}, alice) +} + +func TestDoReqFail(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "", http.StatusInternalServerError) + })) + defer srv.Close() + + clnt := jttp.NewClient(srv.Client()) + url, err := url.Parse(srv.URL) + require.NoError(t, err) + req := &http.Request{Method: http.MethodGet, URL: url} + var alice Person + err = clnt.Do(req, http.StatusOK, &alice) + require.Error(t, err) + require.Equal(t, fmt.Sprintf("jttp: GET %s: 500 Internal Server Error (expected 200)", srv.URL), err.Error()) +} + +func TestDoBadStatus(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(201) + fmt.Fprint(w, `{"name": "Alice", "age": 123}`) + })) + defer srv.Close() + + clnt := jttp.NewClient(srv.Client()) + url, err := url.Parse(srv.URL) + require.NoError(t, err) + req := &http.Request{Method: http.MethodGet, URL: url} + var alice Person + err = clnt.Do(req, 200, &alice) + require.Error(t, err) + require.Equal(t, fmt.Sprintf("jttp: GET %s: 201 Created (expected 200)", srv.URL), err.Error()) +} + +func TestDoDecodeFail(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"name": "Alice", "age": "notanumber"}`) + })) + defer srv.Close() + + clnt := jttp.NewClient(srv.Client()) + url, err := url.Parse(srv.URL) + require.NoError(t, err) + req := &http.Request{Method: http.MethodGet, URL: url} + var alice Person + err = clnt.Do(req, http.StatusOK, &alice) + require.Error(t, err) + require.Contains(t, err.Error(), "json") + require.Contains(t, err.Error(), "Person.Age") +} |