From a9e49906e0bc81ee5e8e82c6746760498c7a9450 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:33:23 -0600 Subject: [PATCH 1/6] feat: initialize client core and ping endpoint establishes the foundational architecture for the Onspring Go SDK. changes included: - add `Client` struct and `NewClient` constructor with authentication. - implement functional options pattern (WithHTTPClient, WithBaseURL, etc.). - add `OnspringAPIError` type for handling non-2xx responses. - implement internal request construction and execution logic. - add `Ping` service for API health check verification. - add comprehensive unit tests and mock server infrastructure. --- client.go | 165 ++++++++++++++++++++++++++++++++++++++++++ client_export_test.go | 19 +++++ client_test.go | 53 ++++++++++++++ error.go | 24 ++++++ error_test.go | 40 ++++++++++ infra_test.go | 34 +++++++++ option.go | 30 ++++++++ option_test.go | 54 ++++++++++++++ ping.go | 46 ++++++++++++ ping_test.go | 103 ++++++++++++++++++++++++++ 10 files changed, 568 insertions(+) create mode 100644 client.go create mode 100644 client_export_test.go create mode 100644 client_test.go create mode 100644 error.go create mode 100644 error_test.go create mode 100644 infra_test.go create mode 100644 option.go create mode 100644 option_test.go create mode 100644 ping.go create mode 100644 ping_test.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..d43da08 --- /dev/null +++ b/client.go @@ -0,0 +1,165 @@ +// 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 ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "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. + defaultAPIKeyHeader = "x-api-key" + // 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 +} + +// 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 ...Option) *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} + + 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 resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return c.handleAPIError(resp) + } + + return 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 +// - body: The request body +// +// Returns: +// - *http.Request: The prepared HTTP request +// - error: An error if the context is nil or request creation fails +func (c *Client) newRequest(ctx context.Context, method, path 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, "/")) + + var bodyReader io.Reader + + req, err := http.NewRequestWithContext(ctx, method, fullURL, 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) + + return req, nil +} diff --git a/client_export_test.go b/client_export_test.go new file mode 100644 index 0000000..6130ab8 --- /dev/null +++ b/client_export_test.go @@ -0,0 +1,19 @@ +package onspring + +import "net/http" + +func (c *Client) HTTPClient() *http.Client { + return c.httpClient +} + +func (c *Client) BaseURL() string { + return c.baseURL +} + +func (c *Client) APIKey() string { + return c.apiKey +} + +func (c *Client) APIVersion() string { + return c.apiVersion +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..5dc695e --- /dev/null +++ b/client_test.go @@ -0,0 +1,53 @@ +package onspring_test + +import ( + "net/http" + "testing" + "time" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestNewClient(t *testing.T) { + t.Run("it should create client with default settings", func(t *testing.T) { + apiKey := "test-api-key" + client := onspring.NewClient(apiKey) + + if client.APIKey() != apiKey { + t.Errorf("Expected apiKey %s, got %s", apiKey, client.APIKey()) + } + + if client.BaseURL() != "https://api.onspring.com" { + t.Errorf("Expected default baseURL, got %s", client.BaseURL()) + } + + if client.HTTPClient().Timeout != 120*time.Second { + t.Errorf("Expected default timeout, got %v", client.HTTPClient().Timeout) + } + + }) + + t.Run("it should create client with custom settings", func(t *testing.T) { + apiKey := "test-api-key" + customURL := "https://custom.onspring.com" + customHTTPClient := &http.Client{Timeout: 50 * time.Second} + + client := onspring.NewClient( + apiKey, + onspring.WithBaseURL(customURL), + onspring.WithHTTPClient(customHTTPClient), + ) + + if client.APIKey() != apiKey { + t.Errorf("Expected apiKey %s, got %s", apiKey, client.APIKey()) + } + + if client.BaseURL() != customURL { + t.Errorf("Expected baseURL %s, got %s", customURL, client.BaseURL()) + } + + if client.HTTPClient() != customHTTPClient { + t.Errorf("Expected custom HTTP client, got %v", client.HTTPClient()) + } + }) +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..45edf4b --- /dev/null +++ b/error.go @@ -0,0 +1,24 @@ +package onspring + +import ( + "fmt" +) + +// OnspringAPIError represents an error returned by the Onspring API. +// It contains the HTTP status code and the error message from the API response. +// This error type implements the error interface. +type OnspringAPIError struct { + // StatusCode is the HTTP status code returned by the API. + StatusCode int + // Message is the error message returned by the API or the HTTP status text. + Message string +} + +// Error returns a formatted error string containing the status code and message. +// It implements the error interface for OnspringAPIError. +// +// Returns: +// - string: A formatted error message in the format "onspring api error: status={code} message={message}" +func (e *OnspringAPIError) Error() string { + return fmt.Sprintf("onspring api error: status=%d message=%s", e.StatusCode, e.Message) +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..3ceeadf --- /dev/null +++ b/error_test.go @@ -0,0 +1,40 @@ +package onspring_test + +import ( + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestOnspringAPIError(t *testing.T) { + t.Run("it should create OnspringAPIError instance", func(t *testing.T) { + statusCode := 500 + message := "Internal Server Error" + + err := &onspring.OnspringAPIError{ + StatusCode: statusCode, + Message: message, + } + + if err.StatusCode != statusCode { + t.Errorf("Expected status code %d, got %d", statusCode, err.StatusCode) + } + + if err.Message != message { + t.Errorf("Expected message %s, got %s", message, err.Message) + } + }) + + t.Run("it should return formatted error message", func(t *testing.T) { + err := &onspring.OnspringAPIError{ + StatusCode: 404, + Message: "Not Found", + } + + expectedMessage := "onspring api error: status=404 message=Not Found" + + if err.Error() != expectedMessage { + t.Errorf("Expected error message %s, got %s", expectedMessage, err.Error()) + } + }) +} diff --git a/infra_test.go b/infra_test.go new file mode 100644 index 0000000..ccf7c2a --- /dev/null +++ b/infra_test.go @@ -0,0 +1,34 @@ +package onspring_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func setupMockServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *onspring.Client) { + t.Helper() + + server := httptest.NewServer(handler) + + t.Cleanup(func() { + server.Close() + }) + + client := onspring.NewClient( + "test-key", + onspring.WithBaseURL(server.URL), + onspring.WithHTTPClient(server.Client()), + ) + + return server, client +} + +type ErrorTransport struct{} + +func (t *ErrorTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, errors.New("simulated network connection failure") +} diff --git a/option.go b/option.go new file mode 100644 index 0000000..1a46134 --- /dev/null +++ b/option.go @@ -0,0 +1,30 @@ +package onspring + +import "net/http" + +// Option is a functional option for configuring a Client. +type Option func(*Client) + +// WithHTTPClient sets a custom HTTP client for the Onspring client. +// If not provided, the client will use http.DefaultClient. +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) { + c.httpClient = client + } +} + +// WithBaseURL sets a custom base URL for the Onspring API. +// This is useful for testing or when using a different Onspring environment. +func WithBaseURL(url string) Option { + return func(c *Client) { + c.baseURL = url + } +} + +// WithAPIVersion sets a custom API version for the Onspring client. +// If not provided, the client will use the default API version. +func WithAPIVersion(version string) Option { + return func(c *Client) { + c.apiVersion = version + } +} diff --git a/option_test.go b/option_test.go new file mode 100644 index 0000000..2c1b1ef --- /dev/null +++ b/option_test.go @@ -0,0 +1,54 @@ +package onspring_test + +import ( + "net/http" + "testing" + "time" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestWithHTTPClient(t *testing.T) { + t.Run("it should set a custom HTTP client on the Onsring client", func(t *testing.T) { + customHTTPClient := &http.Client{Timeout: 10 * time.Second} + + clientWithCustomHTTP := onspring.NewClient( + "test-api-key", + onspring.WithHTTPClient(customHTTPClient), + ) + + if clientWithCustomHTTP.HTTPClient() != customHTTPClient { + t.Errorf("Expected custom HTTP client to be set") + } + }) +} + +func TestWithBaseURL(t *testing.T) { + t.Run("it should set a custom base URL on the Onsring client", func(t *testing.T) { + customBaseURL := "https://custom.onspring.com" + + clientWithCustomURL := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL(customBaseURL), + ) + + if clientWithCustomURL.BaseURL() != customBaseURL { + t.Errorf("Expected base URL to be %s, got %s", customBaseURL, clientWithCustomURL.BaseURL()) + } + }) +} + +func TestWithAPIVersion(t *testing.T) { + t.Run("it should set a custom API version on the Onsring client", func(t *testing.T) { + customVersion := "3.0" + + clientWithCustomVersion := onspring.NewClient( + "test-api-key", + onspring.WithAPIVersion(customVersion), + ) + + if clientWithCustomVersion.APIVersion() != customVersion { + t.Errorf("Expected API version to be %s, got %s", customVersion, clientWithCustomVersion.APIVersion()) + } + }) +} diff --git a/ping.go b/ping.go new file mode 100644 index 0000000..8163798 --- /dev/null +++ b/ping.go @@ -0,0 +1,46 @@ +package onspring + +import ( + "context" + "net/http" +) + +const ( + // pingPath is the API endpoint path for the ping health check. + pingPath = "/ping" +) + +// PingEndpoint provides access to the Onspring API ping endpoint. +// It can be used to verify API connectivity and authentication. +type PingEndpoint struct { + // client is the parent Client used to make API requests. + client *Client +} + +// Get performs a ping request to verify API connectivity. +// This is a simple health check that can be used to test if the API is +// reachable. +// +// Parameters: +// - ctx: The context for the request +// +// Returns: +// - error: nil if the ping succeeds, or an error if the request fails +// or authentication is invalid +// +// Example: +// +// client := onspring.NewClient("your-api-key") +// err := client.Ping.Get(context.Background()) +// if err != nil { +// log.Fatal("Ping failed:", err) +// } +func (p *PingEndpoint) Get(ctx context.Context) error { + req, err := p.client.newRequest(ctx, http.MethodGet, pingPath, nil) + + if err != nil { + return err + } + + return p.client.do(req) +} diff --git a/ping_test.go b/ping_test.go new file mode 100644 index 0000000..38afc99 --- /dev/null +++ b/ping_test.go @@ -0,0 +1,103 @@ +package onspring_test + +import ( + "context" + "net/http" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestGet(t *testing.T) { + t.Run("it should return an error if context is nil", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + err := client.Ping.Get(nil) + + if err == nil { + t.Errorf("Expected error for nil context, got nil") + } + }) + + t.Run("it should return an error if context is canceled", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + err := client.Ping.Get(ctx) + + if err == nil { + t.Errorf("Expected error for canceled context, got nil") + } + }) + + t.Run("it should return an error if encounters a network error", func(t *testing.T) { + client := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://invalid-url"), + onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), + ) + + err := client.Ping.Get(context.Background()) + + if err == nil { + t.Errorf("Expected network error, got nil") + } + }) + + t.Run("it should return an error if create a request fails", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + invalidClient := onspring.NewClient( + "test-api-key", + onspring.WithBaseURL("http://[::1]:namedport"), + onspring.WithHTTPClient(client.HTTPClient()), + ) + + err := invalidClient.Ping.Get(context.Background()) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should perform a GET request to the /ping endpoint and return no error if receives 200 status code", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("Expected GET method, got %s", r.Method) + } + + if r.URL.Path != "/ping" { + t.Errorf("Expected /ping endpoint, got %s", r.URL.Path) + } + + w.WriteHeader(http.StatusOK) + }) + + err := client.Ping.Get(context.Background()) + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("it should return an error if the /ping endpoint returns a non-200 status code", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + err := client.Ping.Get(context.Background()) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) +} From 54865ba64c59597d3023a67601351fc0f0f7492e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:59:46 -0600 Subject: [PATCH 2/6] tests: use proper context in test + add pull request workflow --- .github/workflows/pull_request.yml | 39 ++++++++++++++++++++++++++++++ client.go | 2 +- ping_test.go | 14 ++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..37019f0 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,39 @@ +name: Pull Request +on: + pull_request: + branches: + - main +jobs: + test-and-lint: + name: Test, Format & Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + cache: true + - name: Check formatting + run: | + # Fails if any files are not formatted correctly + if [ -n "$(gofmt -l .)" ]; then + echo "Go code is not formatted:" + gofmt -d . + exit 1 + fi + - name: Run go vet + run: go vet ./... + - name: Run linter + uses: golangci/golangci-lint-action@v9 + with: + version: latest + - name: Run tests + run: go test -v ./... -coverprofile=coverage.out + - name Generate coverage report + run: go tool cover -html=coverage.out -o coverage.html + - name: Upload test coverage + uses: actions/upload-artifact@v6 + with: + name: coverage.html diff --git a/client.go b/client.go index d43da08..3090307 100644 --- a/client.go +++ b/client.go @@ -143,7 +143,7 @@ func (c *Client) handleAPIError(resp *http.Response) error { // Returns: // - *http.Request: The prepared HTTP request // - error: An error if the context is nil or request creation fails -func (c *Client) newRequest(ctx context.Context, method, path string, body any) (*http.Request, error) { +func (c *Client) newRequest(ctx context.Context, method, path string, _ any) (*http.Request, error) { if ctx == nil { return nil, fmt.Errorf("context must not be nil") } diff --git a/ping_test.go b/ping_test.go index 38afc99..62ef142 100644 --- a/ping_test.go +++ b/ping_test.go @@ -14,7 +14,9 @@ func TestGet(t *testing.T) { w.WriteHeader(http.StatusOK) }) - err := client.Ping.Get(nil) + var nilContext context.Context = nil + + err := client.Ping.Get(nilContext) if err == nil { t.Errorf("Expected error for nil context, got nil") @@ -26,7 +28,7 @@ func TestGet(t *testing.T) { w.WriteHeader(http.StatusOK) }) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(t.Context()) cancel() @@ -44,7 +46,7 @@ func TestGet(t *testing.T) { onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), ) - err := client.Ping.Get(context.Background()) + err := client.Ping.Get(t.Context()) if err == nil { t.Errorf("Expected network error, got nil") @@ -62,7 +64,7 @@ func TestGet(t *testing.T) { onspring.WithHTTPClient(client.HTTPClient()), ) - err := invalidClient.Ping.Get(context.Background()) + err := invalidClient.Ping.Get(t.Context()) if err == nil { t.Errorf("Expected request creation error, got nil") @@ -82,7 +84,7 @@ func TestGet(t *testing.T) { w.WriteHeader(http.StatusOK) }) - err := client.Ping.Get(context.Background()) + err := client.Ping.Get(t.Context()) if err != nil { t.Errorf("Expected no error, got %v", err) @@ -94,7 +96,7 @@ func TestGet(t *testing.T) { w.WriteHeader(http.StatusInternalServerError) }) - err := client.Ping.Get(context.Background()) + err := client.Ping.Get(t.Context()) if err == nil { t.Errorf("Expected error, got nil") From 311acd4d2666d2a8dbe8da6a2ff6c1bfcb46bd16 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:14:20 -0600 Subject: [PATCH 3/6] docs: update README.md --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ping_test.go | 1 + 2 files changed, 83 insertions(+) diff --git a/README.md b/README.md index 491d6e9..983b213 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,85 @@ The Go SDK for the Onspring API is meant to simplify development in Go for Onspr Note: This is an unofficial SDK for the Onspring API. It was not built in consultation with Onspring Technologies LLC or a member of their development team. This SDK was developed independently using Onspring's existing C# SDK, the Onspring API's swagger page, and api documentation as the starting point with the intention of making development of integrations done in Javascript with an Onspring instance quicker and more convenient. + +## Dependencies + +### Go + +Requires use of [Go](https://golang.org/dl/) version 1.18 or higher. + +## Installation + +To install the Onspring API Go SDK, use the following command: + +```pwsh +go get github.com/StevanFreeborn/onspring-api-sdk-go +``` + +## API Key + +In order to successfully interact with the Onspring Api you will need an API key. API keys are obtained by an Onspring user with permissions to at least **Read** API Keys for your instance via the following steps: + +1. Login to the Onspring instance. +2. Navigate to **Administration** > **Security** > **API Keys** +3. On the list page, add a new API Key - this will require **Create** permissions - or click an existing API key to view its details. +4. Click on the **Developer Information** tab. +5. Copy the **X-ApiKey Header** value from this tab. + +**Important:** + +- An API Key must have a status of `Enabled` in order to make authorized requests. +- Each API Key must have an assigned Role. This role controls the permissions for requests made. If the API Key used does not have sufficient permissions the requests made won't be successful. + +### 🔒 Permission Considerations + +You can think of any API Key as another user in your Onspring instance and therefore it is subject to all the same permission considerations as any other user when it comes to its ability to access data in your instance. The API Key you use needs to have all the correct permissions within your instance to access the data requested. Things to think about in this context are `role security`, `content security`, and `field security`. + +## Start Coding + +### `Client` + +The most common way to use the SDK is to create a `Client` instance and call its methods to interact with the Onspring API. Here is an example of how to create a `Client` instance. You will need to provide your API key when creating the client. It is best practice to store your API key securely and not hard-code it in your source code. + +```go +import "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" + +client := onspring.NewClient("your-api-key") +``` + +The `Client` instance can be further configured by providing optional configuration settings via the `ClientConfig` struct. For example, you can set a custom base URL for the Onspring API if needed: + +```go +import "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" + +client := onspring.NewClient( + "your-api-key", + onspring.WithBaseURL(customURL), +) +``` + +### Full API Documentation + +You may wish to refer to the full [Onspring API documentation](https://software.onspring.com/hubfs/Training/Admin%20Guide%20-%20v2%20API.pdf) when determining which values to pass as parameters to some of the `OnspringClient` methods. There is also a [swagger page](https://api.onspring.com/swagger/index.html) that you can use for making exploratory requests. + +## Examples + +### Connectivity + +#### Verify connectivity + +```go +import ( + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +err := client.Ping.Get(context.TODO()) + +if err == nil { + fmt.Println("Connection successful!") +} +``` + diff --git a/ping_test.go b/ping_test.go index 62ef142..a3e1214 100644 --- a/ping_test.go +++ b/ping_test.go @@ -46,6 +46,7 @@ func TestGet(t *testing.T) { onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), ) + context.TODO() err := client.Ping.Get(t.Context()) if err == nil { From 6e35c0a7877683fd8ef1f5033ddc74bdda30f079 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:15:38 -0600 Subject: [PATCH 4/6] chore: fix workflow file --- .github/workflows/pull_request.yml | 2 +- ping_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 37019f0..4042fba 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -31,7 +31,7 @@ jobs: version: latest - name: Run tests run: go test -v ./... -coverprofile=coverage.out - - name Generate coverage report + - name: Generate coverage report run: go tool cover -html=coverage.out -o coverage.html - name: Upload test coverage uses: actions/upload-artifact@v6 diff --git a/ping_test.go b/ping_test.go index a3e1214..62ef142 100644 --- a/ping_test.go +++ b/ping_test.go @@ -46,7 +46,6 @@ func TestGet(t *testing.T) { onspring.WithHTTPClient(&http.Client{Transport: &ErrorTransport{}}), ) - context.TODO() err := client.Ping.Get(t.Context()) if err == nil { From 08eb65c8ad6b57413dacdb4e9b8b0eb339e925a1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:18:57 -0600 Subject: [PATCH 5/6] fix: properly discard resp.Body.Close to avoid lint error --- client.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 3090307..480abf2 100644 --- a/client.go +++ b/client.go @@ -95,7 +95,9 @@ func (c *Client) do(req *http.Request) error { return err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return c.handleAPIError(resp) From 60bb3954c50ab00908f27a8d870693890b087ea2 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:20:35 -0600 Subject: [PATCH 6/6] chore: fix workflow file --- .github/workflows/pull_request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4042fba..064e05c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,4 +36,5 @@ jobs: - name: Upload test coverage uses: actions/upload-artifact@v6 with: - name: coverage.html + name: coverage-report + path: coverage.html