Files
onspring-api-sdk-go/client.go
T

319 lines
9.3 KiB
Go
Raw Normal View History

// Package onspring provides a Go SDK for interacting with the Onspring API.
// It offers a type-safe, idiomatic Go interface for making API requests
// to the Onspring platform.
package onspring
import (
2026-03-25 16:27:06 -05:00
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const (
// defaultBaseURL is the default base URL for the Onspring API.
defaultBaseURL = "https://api.onspring.com"
// defaultTimeout is the default HTTP client timeout duration.
defaultTimeout = 120 * time.Second
// defaultAPIKeyHeader is the HTTP header name used for API key authentication.
2026-03-26 16:59:49 -05:00
defaultAPIKeyHeader = "x-apikey"
// defaultAPIVersionHeader is the HTTP header name used to specify the API version.
defaultAPIVersionHeader = "x-api-version"
// defaultAPIVersion is the default Onspring API version to use.
defaultAPIVersion = "2.0"
)
// Client is the main client for interacting with the Onspring API.
// It manages HTTP communication, authentication, and API versioning.
// All API endpoints are accessed through this client.
type Client struct {
// httpClient is the underlying HTTP client used to make requests.
httpClient *http.Client
// baseURL is the base URL for the Onspring API.
baseURL string
// apiKey is the API key used for authentication.
apiKey string
// apiVersion is the API version to use for requests.
apiVersion string
// Ping provides access to the ping endpoint for health checks.
Ping *PingEndpoint
// Apps provides access to the apps within an Onspring instance.
Apps *AppsEndpoint
2026-03-25 16:27:06 -05:00
// Fields provides access to the fields within an Onspring instance.
Fields *FieldsEndpoint
2026-03-26 10:41:52 -05:00
// Lists provides access to the lists within an Onspring instance.
Lists *ListsEndpoint
2026-03-26 11:01:08 -05:00
// Reports provides access to the reports within an Onspring instance.
Reports *ReportsEndpoint
2026-03-26 11:46:30 -05:00
// Files provides access to the files within an Onspring instance.
Files *FilesEndpoint
2026-03-26 15:25:25 -05:00
// Records provides access to the records within an Onspring instance.
Records *RecordsEndpoint
}
// NewClient creates a new Onspring API client with the provided API key.
// It initializes the client with default settings including a 100-second timeout,
// the production API base URL, and API version 2.0.
//
// Optional configuration can be provided using Option functions such as
// WithHTTPClient, WithBaseURL, and WithAPIVersion.
//
// Parameters:
// - apiKey: The API key for authenticating with the Onspring API
// - opts: Optional configuration functions to customize the client
//
// Returns:
// - *Client: A configured Onspring API client ready to make requests
//
// Example:
//
// client := onspring.NewClient("your-api-key")
// client := onspring.NewClient("your-api-key", onspring.WithHTTPClient(customHTTPClient))
func NewClient(apiKey string, opts ...ClientOption) *Client {
c := &Client{
httpClient: &http.Client{Timeout: defaultTimeout},
baseURL: defaultBaseURL,
apiKey: apiKey,
apiVersion: defaultAPIVersion,
}
for _, opt := range opts {
opt(c)
}
c.Ping = &PingEndpoint{client: c}
c.Apps = &AppsEndpoint{client: c}
2026-03-25 16:27:06 -05:00
c.Fields = &FieldsEndpoint{client: c}
2026-03-26 10:41:52 -05:00
c.Lists = &ListsEndpoint{client: c}
2026-03-26 11:01:08 -05:00
c.Reports = &ReportsEndpoint{client: c}
2026-03-26 11:46:30 -05:00
c.Files = &FilesEndpoint{client: c}
2026-03-26 15:25:25 -05:00
c.Records = &RecordsEndpoint{client: c}
return c
}
// do executes an HTTP request and handles the response.
// It performs the actual HTTP call using the configured HTTP client,
// checks the response status code, and handles any API errors.
//
// Parameters:
// - req: The HTTP request to execute
//
// Returns:
// - error: nil if the request succeeds, or an error if the request fails
// or returns a non-2xx status code
func (c *Client) do(req *http.Request) error {
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return c.handleAPIError(resp)
}
return nil
}
// doWithJsonResponse executes an HTTP request and decodes the JSON response.
// It performs the HTTP call, checks the response status code, and decodes
// the JSON response body into the provided variable.
//
// Parameters:
// - req: The HTTP request to execute
// - v: A pointer to the variable where the decoded JSON response will be stored
//
// Returns:
// - error: nil if the request and decoding succeed, or an error if the request fails,
// returns a non-2xx status code, or if JSON decoding fails
func (c *Client) doWithJsonResponse(req *http.Request, v any) error {
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return c.handleAPIError(resp)
}
return json.NewDecoder(resp.Body).Decode(v)
}
2026-03-26 11:46:30 -05:00
// doWithBytesResponse executes an HTTP request and returns the raw response bytes and headers.
// It performs the HTTP call, checks the response status code, and reads
// the response body into a byte slice.
//
// Parameters:
// - req: The HTTP request to execute
//
// Returns:
// - []byte: The raw response body bytes
// - http.Header: The response headers
// - error: nil if the request succeeds, or an error if the request fails
// or returns a non-2xx status code
func (c *Client) doWithBytesResponse(req *http.Request) ([]byte, http.Header, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, nil, c.handleAPIError(resp)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("failed to read response body: %w", err)
}
return data, resp.Header, nil
}
// newMultipartRequest creates a new multipart/form-data HTTP request for the Onspring API.
// It constructs the full URL, sets required authentication headers,
// and prepares the multipart form data with the provided context.
//
// Parameters:
// - ctx: The context for the request
// - path: The API endpoint path
// - body: The request body as a reader (should be a multipart form body)
// - contentType: The content type header value (including boundary)
//
// Returns:
// - *http.Request: The prepared HTTP request
// - error: An error if the context is nil or request creation fails
func (c *Client) newMultipartRequest(ctx context.Context, path string, body io.Reader, contentType string) (*http.Request, error) {
if ctx == nil {
return nil, fmt.Errorf("context must not be nil")
}
fullURL := fmt.Sprintf("%s/%s", strings.TrimRight(c.baseURL, "/"), strings.TrimLeft(path, "/"))
validUrl, urlParsingError := url.Parse(fullURL)
if urlParsingError != nil {
return nil, fmt.Errorf("failed to parse the request url: %w", urlParsingError)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, validUrl.String(), body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set(defaultAPIKeyHeader, c.apiKey)
req.Header.Set(defaultAPIVersionHeader, c.apiVersion)
req.Header.Set("Content-Type", contentType)
return req, nil
}
// handleAPIError processes error responses from the Onspring API.
// It attempts to decode the error message from the response body.
// If decoding fails, it falls back to using the HTTP status text.
//
// Parameters:
// - resp: The HTTP response containing the error
//
// Returns:
// - error: An OnspringAPIError with the status code and error message
func (c *Client) handleAPIError(resp *http.Response) error {
var errBody struct {
Message string `json:"message"`
}
decodeErr := json.NewDecoder(resp.Body).Decode(&errBody)
if decodeErr != nil {
errBody.Message = http.StatusText(resp.StatusCode)
}
return &OnspringAPIError{
StatusCode: resp.StatusCode,
Message: errBody.Message,
}
}
// newRequest creates a new HTTP request for the Onspring API.
// It constructs the full URL, sets required authentication headers,
// and prepares the request with the provided context.
//
// Parameters:
// - ctx: The context for the request
// - method: The HTTP method
// - path: The API endpoint path
// - queryParams: The query parameters for the request
// - body: The request body
//
// Returns:
// - *http.Request: The prepared HTTP request
// - error: An error if the context is nil or request creation fails
2026-03-25 16:27:06 -05:00
func (c *Client) newRequest(ctx context.Context, method, path string, queryParams map[string]string, body any) (*http.Request, error) {
if ctx == nil {
return nil, fmt.Errorf("context must not be nil")
}
fullURL := fmt.Sprintf("%s/%s", strings.TrimRight(c.baseURL, "/"), strings.TrimLeft(path, "/"))
validUrl, urlParsingError := url.Parse(fullURL)
if urlParsingError != nil {
return nil, fmt.Errorf("failed to parse the request url: %w", urlParsingError)
}
q := validUrl.Query()
for key, value := range queryParams {
q.Add(key, value)
}
validUrl.RawQuery = q.Encode()
var bodyReader io.Reader
2026-03-25 16:27:06 -05:00
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewReader(jsonData)
}
req, err := http.NewRequestWithContext(ctx, method, validUrl.String(), bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set(defaultAPIKeyHeader, c.apiKey)
req.Header.Set(defaultAPIVersionHeader, c.apiVersion)
2026-03-25 16:27:06 -05:00
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return req, nil
}