mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 12:31:59 +00:00
122 lines
2.8 KiB
Go
122 lines
2.8 KiB
Go
package resolvespec
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// APIError is the server error object.
|
|
type APIError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Details any `json:"details,omitempty"`
|
|
Detail string `json:"detail,omitempty"` // server-side reason (funcspec / restheadspec)
|
|
SQL string `json:"sql,omitempty"`
|
|
}
|
|
|
|
// Error is returned on a non-2xx response or an unsuccessful result.
|
|
type Error struct {
|
|
StatusCode int
|
|
APIError
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
if e.Message != "" {
|
|
return e.Message
|
|
}
|
|
return fmt.Sprintf("http %d", e.StatusCode)
|
|
}
|
|
|
|
type config struct {
|
|
baseURL string
|
|
token string
|
|
headers http.Header
|
|
http *http.Client
|
|
}
|
|
|
|
// Option configures a client.
|
|
type Option func(*config)
|
|
|
|
func WithToken(token string) Option { return func(c *config) { c.token = token } }
|
|
func WithHTTPClient(h *http.Client) Option { return func(c *config) { c.http = h } }
|
|
func WithHeader(name, value string) Option {
|
|
return func(c *config) { c.headers.Set(name, value) }
|
|
}
|
|
|
|
func newConfig(baseURL string, opts []Option) config {
|
|
c := config{baseURL: strings.TrimRight(baseURL, "/"), headers: http.Header{}, http: &http.Client{Timeout: 30 * time.Second}}
|
|
for _, o := range opts {
|
|
o(&c)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// headers: Content-Type < custom headers < bearer token.
|
|
func (c *config) newRequest(ctx context.Context, method, u string, body []byte) (*http.Request, error) {
|
|
var r io.Reader
|
|
if body != nil {
|
|
r = bytes.NewReader(body)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, u, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for k, vs := range c.headers {
|
|
req.Header[k] = append([]string(nil), vs...)
|
|
}
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (c *config) do(req *http.Request) (*http.Response, []byte, error) {
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
b, err := io.ReadAll(resp.Body)
|
|
return resp, b, err
|
|
}
|
|
|
|
func errorFrom(status int, body []byte) *Error {
|
|
e := &Error{StatusCode: status}
|
|
var env struct {
|
|
Error *APIError `json:"error"`
|
|
}
|
|
if json.Unmarshal(body, &env) == nil && env.Error != nil {
|
|
e.APIError = *env.Error
|
|
}
|
|
if e.Message == "" {
|
|
text := ""
|
|
if !json.Valid(body) {
|
|
text = strings.TrimSpace(string(body))
|
|
if len(text) > 200 {
|
|
text = text[:200]
|
|
}
|
|
}
|
|
if text == "" {
|
|
text = fmt.Sprintf("%s (%d)", http.StatusText(status), status)
|
|
}
|
|
e.Message = text
|
|
}
|
|
return e
|
|
}
|
|
|
|
func buildURL(base, schema, entity string, id string) string {
|
|
u := base + "/" + url.PathEscape(schema) + "/" + url.PathEscape(entity)
|
|
if id != "" {
|
|
u += "/" + url.PathEscape(id)
|
|
}
|
|
return u
|
|
}
|