diff --git a/client.go b/client.go index 5741400..aceb3b6 100644 --- a/client.go +++ b/client.go @@ -9,201 +9,110 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" - "io" "net/http" "net/url" "time" ) const ( - airtableBaseURL = "https://api.airtable.com/v0" - rateLimit = 4 + defaultBaseURL = "https://api.airtable.com/v0" + defaultRateLimit = 4 ) // Client client for airtable api. type Client struct { - client *http.Client + Client *http.Client + BaseURL string + APIKey string 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 // https://airtable.com/account -func NewClient(apiKey string) *Client { - return &Client{ - client: http.DefaultClient, - rateLimiter: time.Tick(time.Second / time.Duration(rateLimit)), - apiKey: apiKey, - baseURL: airtableBaseURL, +func New(apiKey string) *Client { + c := &Client{ + Client: http.DefaultClient, + APIKey: apiKey, + BaseURL: defaultBaseURL, } -} -// Set custom http client for custom usage -func (at *Client) SetCustomClient(client *http.Client) { - at.client = client + c.SetRateLimit(defaultRateLimit) + + return c } // SetRateLimit rate limit setter for custom usage // Airtable limit is 5 requests per second (we use 4) // https://airtable.com/{yourDatabaseID}/api/docs#curl/ratelimits -func (at *Client) SetRateLimit(customRateLimit int) { - at.rateLimiter = time.Tick(time.Second / time.Duration(customRateLimit)) +func (at *Client) SetRateLimit(rateLimit int) { + at.rateLimiter = time.Tick(time.Second / time.Duration(rateLimit)) } -func (at *Client) SetBaseURL(baseURL string) error { - 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() { +func (at *Client) waitForRateLimit() { <-at.rateLimiter } func (at *Client) get(ctx context.Context, db, table, recordID string, params url.Values, target any) error { - at.rateLimit() - - 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 + return at.do(ctx, "GET", db, table, recordID, params, nil, target) } -func (at *Client) post(ctx context.Context, db, table string, 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, "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) post(ctx context.Context, db, table string, data, target any) error { + return at.do(ctx, "POST", db, table, "", nil, data, target) } 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{} for _, recordID := range recordIDs { 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 { 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) 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("Authorization", fmt.Sprintf("Bearer %s", at.apiKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", at.APIKey)) - return at.do(req, response) -} - -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) + resp, err := at.Client.Do(req) if err != nil { - return fmt.Errorf("cannot marshal body: %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) + return fmt.Errorf("http request failed: %w", err) } defer resp.Body.Close() @@ -212,14 +121,11 @@ func (at *Client) do(req *http.Request, response any) error { return makeHTTPClientError(url, resp) } - b, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("HTTP Read error on response for %s: %w", url, err) - } + dec := json.NewDecoder(resp.Body) - err = json.Unmarshal(b, response) + err = dec.Decode(target) 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