Drastically simplify client

This commit is contained in:
Ian Gulliver
2024-06-22 15:13:30 -07:00
parent cd3ae33c49
commit 8ea1e9731b

210
client.go
View File

@@ -9,201 +9,110 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
) )
const ( const (
airtableBaseURL = "https://api.airtable.com/v0" defaultBaseURL = "https://api.airtable.com/v0"
rateLimit = 4 defaultRateLimit = 4
) )
// Client client for airtable api. // Client client for airtable api.
type Client struct { type Client struct {
client *http.Client Client *http.Client
BaseURL string
APIKey string
rateLimiter <-chan time.Time rateLimiter <-chan time.Time
baseURL string
apiKey string
} }
// NewClient airtable client constructor // New airtable client constructor
// your API KEY you can get on your account page // your API KEY you can get on your account page
// https://airtable.com/account // https://airtable.com/account
func NewClient(apiKey string) *Client { func New(apiKey string) *Client {
return &Client{ c := &Client{
client: http.DefaultClient, Client: http.DefaultClient,
rateLimiter: time.Tick(time.Second / time.Duration(rateLimit)), APIKey: apiKey,
apiKey: apiKey, BaseURL: defaultBaseURL,
baseURL: airtableBaseURL,
} }
}
// Set custom http client for custom usage c.SetRateLimit(defaultRateLimit)
func (at *Client) SetCustomClient(client *http.Client) {
at.client = client return c
} }
// SetRateLimit rate limit setter for custom usage // SetRateLimit rate limit setter for custom usage
// Airtable limit is 5 requests per second (we use 4) // Airtable limit is 5 requests per second (we use 4)
// https://airtable.com/{yourDatabaseID}/api/docs#curl/ratelimits // https://airtable.com/{yourDatabaseID}/api/docs#curl/ratelimits
func (at *Client) SetRateLimit(customRateLimit int) { func (at *Client) SetRateLimit(rateLimit int) {
at.rateLimiter = time.Tick(time.Second / time.Duration(customRateLimit)) at.rateLimiter = time.Tick(time.Second / time.Duration(rateLimit))
} }
func (at *Client) SetBaseURL(baseURL string) error { func (at *Client) waitForRateLimit() {
url, err := url.Parse(baseURL)
if err != nil {
return fmt.Errorf("failed to parse baseURL: %s", err)
}
if url.Scheme == "" {
return fmt.Errorf("scheme of http or https must be specified")
}
if url.Scheme != "https" && url.Scheme != "http" {
return fmt.Errorf("http or https baseURL must be used")
}
at.baseURL = url.String()
return nil
}
func (at *Client) rateLimit() {
<-at.rateLimiter <-at.rateLimiter
} }
func (at *Client) get(ctx context.Context, db, table, recordID string, params url.Values, target any) error { func (at *Client) get(ctx context.Context, db, table, recordID string, params url.Values, target any) error {
at.rateLimit() return at.do(ctx, "GET", db, table, recordID, params, nil, target)
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
if recordID != "" {
url += fmt.Sprintf("/%s", recordID)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
req.URL.RawQuery = params.Encode()
err = at.do(req, target)
if err != nil {
return err
}
return nil
} }
func (at *Client) post(ctx context.Context, db, table string, data, response any) error { func (at *Client) post(ctx context.Context, db, table string, data, target any) error {
at.rateLimit() return at.do(ctx, "POST", db, table, "", nil, data, target)
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
body, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("cannot marshal body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
return at.do(req, response)
} }
func (at *Client) delete(ctx context.Context, db, table string, recordIDs []string, target any) error { func (at *Client) delete(ctx context.Context, db, table string, recordIDs []string, target any) error {
at.rateLimit()
rawURL := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
params := url.Values{} params := url.Values{}
for _, recordID := range recordIDs { for _, recordID := range recordIDs {
params.Add("records[]", recordID) params.Add("records[]", recordID)
} }
req, err := http.NewRequestWithContext(ctx, "DELETE", rawURL, nil) return at.do(ctx, "DELETE", db, table, "", params, nil, target)
}
func (at *Client) patch(ctx context.Context, db, table string, data, target any) error {
return at.do(ctx, "PATCH", db, table, "", nil, data, target)
}
func (at *Client) put(ctx context.Context, db, table string, data, target any) error {
return at.do(ctx, "PUT", db, table, "", nil, data, target)
}
func (at *Client) do(ctx context.Context, method, db, table, recordID string, params url.Values, data, target any) error {
var err error
at.waitForRateLimit()
url := fmt.Sprintf("%s/%s/%s", at.BaseURL, db, table)
if recordID != "" {
url += fmt.Sprintf("/%s", recordID)
}
body := []byte{}
if data != nil {
body, err = json.Marshal(data)
if err != nil {
return fmt.Errorf("marshalling message body: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil { if err != nil {
return fmt.Errorf("cannot create request: %w", err) return fmt.Errorf("cannot create request: %w", err)
} }
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
err = at.do(req, target)
if err != nil {
return err
}
return nil
}
func (at *Client) patch(ctx context.Context, db, table, data, response any) error {
at.rateLimit()
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
body, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("cannot marshal body: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "PATCH", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.APIKey))
return at.do(req, response) resp, err := at.Client.Do(req)
}
func (at *Client) put(ctx context.Context, db, table, data, response any) error {
at.rateLimit()
url := fmt.Sprintf("%s/%s/%s", at.baseURL, db, table)
body, err := json.Marshal(data)
if err != nil { if err != nil {
return fmt.Errorf("cannot marshal body: %w", err) return fmt.Errorf("http request failed: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.apiKey))
return at.do(req, response)
}
func (at *Client) do(req *http.Request, response any) error {
if req == nil {
return errors.New("nil request")
}
url := req.URL.RequestURI()
resp, err := at.client.Do(req)
if err != nil {
return fmt.Errorf("HTTP request failure on %s: %w", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -212,14 +121,11 @@ func (at *Client) do(req *http.Request, response any) error {
return makeHTTPClientError(url, resp) return makeHTTPClientError(url, resp)
} }
b, err := io.ReadAll(resp.Body) dec := json.NewDecoder(resp.Body)
if err != nil {
return fmt.Errorf("HTTP Read error on response for %s: %w", url, err)
}
err = json.Unmarshal(b, response) err = dec.Decode(target)
if err != nil { if err != nil {
return fmt.Errorf("JSON decode failed on %s:\n%s\nerror: %w", url, string(b), err) return fmt.Errorf("json decode failed: %w", err)
} }
return nil return nil