File: jsonclient.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.8.15-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,200 kB
  • sloc: sh: 49; makefile: 5
file content (165 lines) | stat: -rw-r--r-- 4,135 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package jsonclient

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
)

// ContentType defines the default MIME type for JSON requests.
const ContentType = "application/json"

// HTTPClientErrorThreshold specifies the status code threshold for client errors (400+).
const HTTPClientErrorThreshold = 400

// ErrUnexpectedStatus indicates an unexpected HTTP response status.
var (
	ErrUnexpectedStatus = errors.New("got unexpected HTTP status")
)

// DefaultClient provides a singleton JSON client using http.DefaultClient.
var DefaultClient = NewClient()

// Client wraps http.Client for JSON operations.
type client struct {
	httpClient *http.Client
	headers    http.Header
	indent     string
}

// Get fetches a URL using GET and unmarshals the response into the provided object using DefaultClient.
func Get(url string, response any) error {
	if err := DefaultClient.Get(url, response); err != nil {
		return fmt.Errorf("getting JSON from %q: %w", url, err)
	}

	return nil
}

// Post sends a request as JSON and unmarshals the response into the provided object using DefaultClient.
func Post(url string, request any, response any) error {
	if err := DefaultClient.Post(url, request, response); err != nil {
		return fmt.Errorf("posting JSON to %q: %w", url, err)
	}

	return nil
}

// NewClient creates a new JSON client using the default http.Client.
func NewClient() Client {
	return NewWithHTTPClient(http.DefaultClient)
}

// NewWithHTTPClient creates a new JSON client using the specified http.Client.
func NewWithHTTPClient(httpClient *http.Client) Client {
	return &client{
		httpClient: httpClient,
		headers: http.Header{
			"Content-Type": []string{ContentType},
		},
	}
}

// Headers returns the default headers for requests.
func (c *client) Headers() http.Header {
	return c.headers
}

// Get fetches a URL using GET and unmarshals the response into the provided object.
func (c *client) Get(url string, response any) error {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
	if err != nil {
		return fmt.Errorf("creating GET request for %q: %w", url, err)
	}

	for key, val := range c.headers {
		req.Header.Set(key, val[0])
	}

	res, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("executing GET request to %q: %w", url, err)
	}

	return parseResponse(res, response)
}

// Post sends a request as JSON and unmarshals the response into the provided object.
func (c *client) Post(url string, request any, response any) error {
	var err error

	var body []byte

	if strReq, ok := request.(string); ok {
		// If the request is a string, pass it through without serializing
		body = []byte(strReq)
	} else {
		body, err = json.MarshalIndent(request, "", c.indent)
		if err != nil {
			return fmt.Errorf("marshaling request to JSON: %w", err)
		}
	}

	req, err := http.NewRequestWithContext(
		context.Background(),
		http.MethodPost,
		url,
		bytes.NewReader(body),
	)
	if err != nil {
		return fmt.Errorf("creating POST request for %q: %w", url, err)
	}

	for key, val := range c.headers {
		req.Header.Set(key, val[0])
	}

	res, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("sending POST request to %q: %w", url, err)
	}

	return parseResponse(res, response)
}

// ErrorResponse checks if an error is a JSON error and unmarshals its body into the response.
func (c *client) ErrorResponse(err error, response any) bool {
	var errMsg Error
	if errors.As(err, &errMsg) {
		return json.Unmarshal([]byte(errMsg.Body), response) == nil
	}

	return false
}

// parseResponse parses the HTTP response and unmarshals it into the provided object.
func parseResponse(res *http.Response, response any) error {
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)

	if res.StatusCode >= HTTPClientErrorThreshold {
		err = fmt.Errorf("%w: %v", ErrUnexpectedStatus, res.Status)
	}

	if err == nil {
		err = json.Unmarshal(body, response)
	}

	if err != nil {
		if body == nil {
			body = []byte{}
		}

		return Error{
			StatusCode: res.StatusCode,
			Body:       string(body),
			err:        err,
		}
	}

	return nil
}