aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README9
-rw-r--r--go.mod10
-rw-r--r--go.sum9
-rw-r--r--jttp.go34
-rw-r--r--jttp_test.go84
5 files changed, 146 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..68586f3
--- /dev/null
+++ b/README
@@ -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.
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a7e8a5d
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..cc8b3f4
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/jttp.go b/jttp.go
new file mode 100644
index 0000000..34c6fd6
--- /dev/null
+++ b/jttp.go
@@ -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")
+}