Drastically simplify client
This commit is contained in:
210
client.go
210
client.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user