Merge pull request #2 from StevanFreeborn/stevanfreeborn/feat/implement-apps-endpoints

feat: implement apps endpoints
This commit is contained in:
Stevan Freeborn
2026-03-23 16:46:46 -05:00
committed by GitHub
11 changed files with 1066 additions and 93 deletions
+2
View File
@@ -0,0 +1,2 @@
*.go text eol=lf
+61 -4
View File
@@ -12,7 +12,7 @@ This SDK was developed independently using Onspring's existing C# SDK, the Onspr
### Go
Requires use of [Go](https://golang.org/dl/) version 1.18 or higher.
Requires use of [Go](https://golang.org/dl/) version 1.23 or higher.
## Installation
@@ -76,8 +76,8 @@ You may wish to refer to the full [Onspring API documentation](https://software.
```go
import (
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
@@ -85,7 +85,64 @@ client := onspring.NewClient("your-api-key")
err := client.Ping.Get(context.TODO())
if err == nil {
fmt.Println("Connection successful!")
fmt.Println("Connection successful!")
}
```
### Apps
#### Get App by Id
```go
import (
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
app, err := client.Apps.Get(context.TODO(), 1)
```
#### Get Apps by Page
##### Retrieve a single page
```go
import (
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
page, err := client.Apps.List(t.Context())
```
##### Retrieve all pages
```go
import (
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
for app, err := range client.Apps.ListAll(t.Context()) {
// Do stuff
}
```
#### Get Apps by Batch
```go
import (
"fmt"
"github.com/StevanFreeborn/onspring-api-sdk-go/onspring"
)
client := onspring.NewClient("your-api-key")
batch, err := client.Apps.GetMany(t.Context(), []int{ 1 })
```
+171
View File
@@ -0,0 +1,171 @@
package onspring
import (
"context"
"fmt"
"iter"
"net/http"
)
const (
appsPath = "/apps"
)
// AppsEndpoint provides access to apps in an Onspring instance.
type AppsEndpoint struct {
client *Client
}
// App represents an Onspring app
type App struct {
Href string `json:"href"`
Id int `json:"id"`
Name string `json:"name"`
}
// AppBatch represents a batch of Onspring apps
type AppBatch struct {
Count int `json:"count"`
Items []App `json:"items"`
}
// List retrieves a paginated list of apps from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize)
//
// Returns:
// - Page[App]: A page of apps with pagination metadata
// - error: An error if the request fails
func (a *AppsEndpoint) List(ctx context.Context, pagingOpts ...PagingOption) (Page[App], error) {
pagingRequest := createPagingRequest(pagingOpts)
req, requestCreationErr := a.client.newRequest(ctx, http.MethodGet, appsPath, pagingRequest.ToParams(), nil)
var page Page[App]
if requestCreationErr != nil {
return page, requestCreationErr
}
responseErr := a.client.doWithJsonResponse(req, &page)
if responseErr != nil {
return page, responseErr
}
return page, nil
}
// ListAll returns an iterator that yields all Apps across all pages.
// It automatically handles pagination by making sequential calls to Get
// until all items have been retrieved or the caller stops the iteration.
//
// The iterator yields each *App and any error encountered during fetching.
// If an error occurs during a page request, the error is yielded and
// iteration terminates.
//
// Parameters:
// - ctx: The context for the request
// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize)
//
// Returns:
// - iter.Seq2[App, error]: An iterator yielding:
// - App: The individual application record.
// - error: An error if a specific page request fails during iteration.
func (a *AppsEndpoint) ListAll(ctx context.Context, pagingOpts ...PagingOption) iter.Seq2[App, error] {
return func(yield func(App, error) bool) {
pagingRequest := createPagingRequest(pagingOpts)
for {
page, err := a.List(ctx, ForPageNumber(pagingRequest.PageNumber), WithPageSize(pagingRequest.PageSize))
if err != nil {
yield(App{}, err)
return
}
for _, item := range page.Items {
if !yield(item, nil) {
return
}
}
if page.TotalPages == page.PageNumber {
break
}
pagingRequest.PageNumber++
}
}
}
// GetMany retrieves a batch of apps from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - ids: The ids of the apps to retrieve
//
// Returns:
// - AppBatch: A batch of apps
// - error: An error if the request fails
func (a *AppsEndpoint) GetMany(ctx context.Context, ids []int) (AppBatch, error) {
path := fmt.Sprintf("%s/batch-get", appsPath)
req, requestCreationErr := a.client.newRequest(ctx, http.MethodPost, path, nil, ids)
var appBatch AppBatch
if requestCreationErr != nil {
return appBatch, requestCreationErr
}
responseErr := a.client.doWithJsonResponse(req, &appBatch)
if responseErr != nil {
return appBatch, responseErr
}
return appBatch, nil
}
// Get retrieves an app from the Onspring API.
//
// Parameters:
// - ctx: The context for the request
// - id: The id of the app to retrieve
//
// Returns:
// - App: An app
// - error: An error if the request fails
func (a *AppsEndpoint) Get(ctx context.Context, id int) (App, error) {
path := fmt.Sprintf("%s/id/%d", appsPath, id)
req, requestCreationErr := a.client.newRequest(ctx, http.MethodGet, path, nil, nil)
var app App
if requestCreationErr != nil {
return app, requestCreationErr
}
responseErr := a.client.doWithJsonResponse(req, &app)
if responseErr != nil {
return app, responseErr
}
return app, nil
}
func createPagingRequest(pagingOpts []PagingOption) *PagingRequest {
pagingRequest := &PagingRequest{
PageNumber: 1,
PageSize: 50,
}
for _, opt := range pagingOpts {
opt(pagingRequest)
}
return pagingRequest
}
+648
View File
@@ -0,0 +1,648 @@
package onspring_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"slices"
"strconv"
"testing"
"github.com/StevanFreeborn/onspring-api-sdk-go"
)
func TestApps(t *testing.T) {
t.Run("List", func(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)
})
var nilContext context.Context = nil
_, err := client.Apps.List(nilContext)
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(t.Context())
cancel()
_, err := client.Apps.List(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.Apps.List(t.Context())
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.Apps.List(t.Context())
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should perform a GET request to the /apps endpoint and return page of apps if receives 200 status code", func(t *testing.T) {
expectedPageNumber := 1
expectedPageSize := 50
expectedPage := onspring.Page[onspring.App]{
TotalPages: 1,
TotalRecords: 1,
PageNumber: 1,
PageSize: 1,
Items: []onspring.App{
{
Href: "https://test.com",
Id: 1,
Name: "App",
},
},
}
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
pageSize := r.URL.Query().Get("pageSize")
if pageNumber != strconv.Itoa(expectedPageNumber) {
t.Errorf("Expected query param pageNumber to be %d but got %s", expectedPageNumber, pageNumber)
}
if pageSize != strconv.Itoa(expectedPageSize) {
t.Errorf("Expected query param pageSize to be %d but got %s", expectedPageSize, pageSize)
}
jsonData, _ := json.Marshal(expectedPage)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
page, err := client.Apps.List(t.Context())
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if !reflect.DeepEqual(expectedPage, page) {
t.Errorf("Expected %v but got %v", expectedPage, page)
}
})
t.Run("it should perform a GET request to the /apps endpoint with non-default paging information when provided", func(t *testing.T) {
expectedPageNumber := 2
expectedPageSize := 1
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
pageSize := r.URL.Query().Get("pageSize")
if pageNumber != strconv.Itoa(expectedPageNumber) {
t.Errorf("Expected query param pageNumber to be %d but got %s", expectedPageNumber, pageNumber)
}
if pageSize != strconv.Itoa(expectedPageSize) {
t.Errorf("Expected query param pageSize to be %d but got %s", expectedPageSize, pageSize)
}
w.WriteHeader(http.StatusOK)
})
_, _ = client.Apps.List(
t.Context(),
onspring.ForPageNumber(expectedPageNumber),
onspring.WithPageSize(expectedPageSize),
)
})
t.Run("it should return an error if the /apps 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.Apps.List(t.Context())
if err == nil {
t.Errorf("Expected error, got nil")
}
})
})
t.Run("ListAll", func(t *testing.T) {
t.Run("it should return an error if fails to retrieve any pages of apps", func(t *testing.T) {
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
for _, err := range client.Apps.ListAll(t.Context()) {
if err == nil {
t.Errorf("Expected error, got nil")
}
}
})
t.Run("it should return all the apps from multiple pages", func(t *testing.T) {
expectedApps := []onspring.App{
{
Href: "https://test.com",
Id: 1,
Name: "App",
},
{
Href: "https://test.com",
Id: 2,
Name: "App",
},
}
pageOne := onspring.Page[onspring.App]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 1,
PageSize: 1,
Items: []onspring.App{expectedApps[0]},
}
pageTwo := onspring.Page[onspring.App]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 2,
PageSize: 1,
Items: []onspring.App{expectedApps[1]},
}
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "1" {
jsonData, _ := json.Marshal(pageOne)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
if pageNumber == "2" {
jsonData, _ := json.Marshal(pageTwo)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedApps := []onspring.App{}
for app, _ := range client.Apps.ListAll(t.Context()) {
retrievedApps = append(retrievedApps, app)
}
if !slices.Equal(expectedApps, retrievedApps) {
t.Errorf("Expected %v but got %v", expectedApps, retrievedApps)
}
})
t.Run("it should return apps and errors if some pages fail and some succeed", func(t *testing.T) {
expectedApps := []onspring.App{
{
Href: "https://test.com",
Id: 1,
Name: "App",
},
}
pageOne := onspring.Page[onspring.App]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 1,
PageSize: 1,
Items: []onspring.App{expectedApps[0]},
}
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "1" {
jsonData, _ := json.Marshal(pageOne)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
if pageNumber == "2" {
w.WriteHeader(http.StatusInternalServerError)
}
})
retrievedApps := []onspring.App{}
encounteredErrors := []error{}
for app, err := range client.Apps.ListAll(t.Context()) {
if err != nil {
encounteredErrors = append(encounteredErrors, err)
} else {
retrievedApps = append(retrievedApps, app)
}
}
if !slices.Equal(expectedApps, retrievedApps) {
t.Errorf("Expected %v but got %v", expectedApps, retrievedApps)
}
if len(encounteredErrors) != 1 {
t.Errorf("Expected to receive one error, but received %d", len(encounteredErrors))
}
})
t.Run("it should start paging from specified page number when given", func(t *testing.T) {
expectedApps := []onspring.App{
{
Href: "https://test.com",
Id: 2,
Name: "App",
},
}
pageTwo := onspring.Page[onspring.App]{
TotalPages: 2,
TotalRecords: 2,
PageNumber: 2,
PageSize: 1,
Items: []onspring.App{expectedApps[0]},
}
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageNumber := r.URL.Query().Get("pageNumber")
if pageNumber == "2" {
jsonData, _ := json.Marshal(pageTwo)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedApps := []onspring.App{}
for app, _ := range client.Apps.ListAll(t.Context(), onspring.ForPageNumber(2)) {
retrievedApps = append(retrievedApps, app)
}
if !slices.Equal(expectedApps, retrievedApps) {
t.Errorf("Expected %v but got %v", expectedApps, retrievedApps)
}
})
t.Run("it should retrieve pages using specified page size when given", func(t *testing.T) {
expectedApps := []onspring.App{
{
Href: "https://test.com",
Id: 1,
Name: "App",
},
{
Href: "https://test.com",
Id: 2,
Name: "App",
},
}
page := onspring.Page[onspring.App]{
TotalPages: 1,
TotalRecords: 2,
PageNumber: 1,
PageSize: 2,
Items: expectedApps,
}
_, 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 != "/apps" {
t.Errorf("Expected /apps endpoint, got %s", r.URL.Path)
}
pageSize := r.URL.Query().Get("pageSize")
if pageSize == "2" {
jsonData, _ := json.Marshal(page)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
}
})
retrievedApps := []onspring.App{}
for app, _ := range client.Apps.ListAll(t.Context(), onspring.WithPageSize(2)) {
retrievedApps = append(retrievedApps, app)
}
if !slices.Equal(expectedApps, retrievedApps) {
t.Errorf("Expected %v but got %v", expectedApps, retrievedApps)
}
})
})
t.Run("GetMany", func(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)
})
var nilContext context.Context = nil
_, err := client.Apps.GetMany(nilContext, []int{})
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(t.Context())
cancel()
_, err := client.Apps.GetMany(ctx, []int{})
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.Apps.GetMany(t.Context(), []int{})
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.Apps.GetMany(t.Context(), []int{})
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should return an error if the /apps/batch-get 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.Apps.GetMany(t.Context(), []int{})
if err == nil {
t.Errorf("Expected error, got nil")
}
})
t.Run("it should return a batch of apps when the /apps/batch-get endpoint returns a 200 status code", func(t *testing.T) {
apps := []onspring.App{
{
Href: "https://test.com",
Id: 1,
Name: "App",
},
}
expectedBatch := onspring.AppBatch{
Count: len(apps),
Items: apps,
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("Expected POST method, got %s", r.Method)
}
if r.URL.Path != "/apps/batch-get" {
t.Errorf("Expected /apps/batch-get endpoint, got %s", r.URL.Path)
}
jsonData, _ := json.Marshal(expectedBatch)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
batch, _ := client.Apps.GetMany(t.Context(), []int{apps[0].Id})
if !reflect.DeepEqual(expectedBatch, batch) {
t.Errorf("Expected %v but got %v", expectedBatch, batch)
}
})
})
t.Run("Get", func(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)
})
var nilContext context.Context = nil
_, err := client.Apps.Get(nilContext, 0)
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(t.Context())
cancel()
_, err := client.Apps.Get(ctx, 0)
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.Apps.Get(t.Context(), 0)
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.Apps.Get(t.Context(), 0)
if err == nil {
t.Errorf("Expected request creation error, got nil")
}
})
t.Run("it should return an error if the /apps/id/:id 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.Apps.Get(t.Context(), 0)
if err == nil {
t.Errorf("Expected error, got nil")
}
})
t.Run("it should return an app if the /apps/id/:id endpoint returns a 200 status code", func(t *testing.T) {
expectedApp := onspring.App{
Href: "https://test.com",
Id: 1,
Name: "App",
}
_, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Expected GET method, got %s", r.Method)
}
expectedPath := fmt.Sprintf("/apps/id/%d", expectedApp.Id)
if r.URL.Path != expectedPath {
t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path)
}
jsonData, _ := json.Marshal(expectedApp)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonData)
})
app, _ := client.Apps.Get(t.Context(), expectedApp.Id)
if !reflect.DeepEqual(expectedApp, app) {
t.Errorf("Expected %v but got %v", expectedApp, app)
}
})
})
}
+50 -3
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
@@ -41,6 +42,8 @@ type Client struct {
// Ping provides access to the ping endpoint for health checks.
Ping *PingEndpoint
// Apps provides access to the apps within an Onspring instance.
Apps *AppsEndpoint
}
// NewClient creates a new Onspring API client with the provided API key.
@@ -61,7 +64,7 @@ type Client struct {
//
// client := onspring.NewClient("your-api-key")
// client := onspring.NewClient("your-api-key", onspring.WithHTTPClient(customHTTPClient))
func NewClient(apiKey string, opts ...Option) *Client {
func NewClient(apiKey string, opts ...ClientOption) *Client {
c := &Client{
httpClient: &http.Client{Timeout: defaultTimeout},
baseURL: defaultBaseURL,
@@ -74,6 +77,7 @@ func NewClient(apiKey string, opts ...Option) *Client {
}
c.Ping = &PingEndpoint{client: c}
c.Apps = &AppsEndpoint{client: c}
return c
}
@@ -106,6 +110,35 @@ func (c *Client) do(req *http.Request) error {
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)
}
// 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.
@@ -140,21 +173,35 @@ func (c *Client) handleAPIError(resp *http.Response) error {
// - 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
func (c *Client) newRequest(ctx context.Context, method, path string, _ any) (*http.Request, error) {
func (c *Client) newRequest(ctx context.Context, method, path string, queryParams map[string]string, _ 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
req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
req, err := http.NewRequestWithContext(ctx, method, validUrl.String(), bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
+5 -5
View File
@@ -2,12 +2,12 @@ package onspring
import "net/http"
// Option is a functional option for configuring a Client.
type Option func(*Client)
// ClientOption is a functional option for configuring a Client.
type ClientOption 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 {
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = client
}
@@ -15,7 +15,7 @@ func WithHTTPClient(client *http.Client) Option {
// 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 {
func WithBaseURL(url string) ClientOption {
return func(c *Client) {
c.baseURL = url
}
@@ -23,7 +23,7 @@ func WithBaseURL(url string) Option {
// 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 {
func WithAPIVersion(version string) ClientOption {
return func(c *Client) {
c.apiVersion = version
}
+10
View File
@@ -0,0 +1,10 @@
package onspring
// Page represents a paginated response from the Onspring API.
type Page[T any] struct {
PageNumber int `json:"pageNumber"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
TotalRecords int `json:"totalRecords"`
Items []T `json:"items"`
}
+36
View File
@@ -0,0 +1,36 @@
package onspring
import "strconv"
// PagingRequest contains pagination parameters for API requests.
type PagingRequest struct {
// The page number to retrieve
PageNumber int
// The size of pages to retrieve
PageSize int
}
// ToParams converts the paging request to a map of query parameters.
func (pr *PagingRequest) ToParams() map[string]string {
return map[string]string{
"pageNumber": strconv.Itoa(pr.PageNumber),
"pageSize": strconv.Itoa(pr.PageSize),
}
}
// PagingOption is a function that modifies a PagingRequest.
type PagingOption func(*PagingRequest)
// ForPageNumber sets the page number for a paging request.
func ForPageNumber(pageNumber int) PagingOption {
return func(pr *PagingRequest) {
pr.PageNumber = pageNumber
}
}
// WithPageSize sets the page size for a paging request.
func WithPageSize(pageSize int) PagingOption {
return func(pr *PagingRequest) {
pr.PageSize = pageSize
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ type PingEndpoint struct {
// log.Fatal("Ping failed:", err)
// }
func (p *PingEndpoint) Get(ctx context.Context) error {
req, err := p.client.newRequest(ctx, http.MethodGet, pingPath, nil)
req, err := p.client.newRequest(ctx, http.MethodGet, pingPath, nil, nil)
if err != nil {
return err
+82 -80
View File
@@ -8,98 +8,100 @@ import (
"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)
})
func TestPing(t *testing.T) {
t.Run("Get", func(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)
})
var nilContext context.Context = nil
var nilContext context.Context = nil
err := client.Ping.Get(nilContext)
err := client.Ping.Get(nilContext)
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(t.Context())
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(t.Context())
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(t.Context())
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 err == nil {
t.Errorf("Expected error for nil context, got nil")
}
})
if r.URL.Path != "/ping" {
t.Errorf("Expected /ping endpoint, got %s", r.URL.Path)
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(t.Context())
cancel()
err := client.Ping.Get(ctx)
if err == nil {
t.Errorf("Expected error for canceled context, got nil")
}
w.WriteHeader(http.StatusOK)
})
err := client.Ping.Get(t.Context())
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{}}),
)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
})
err := client.Ping.Get(t.Context())
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)
if err == nil {
t.Errorf("Expected network error, got nil")
}
})
err := client.Ping.Get(t.Context())
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)
})
if err == nil {
t.Errorf("Expected error, got nil")
}
invalidClient := onspring.NewClient(
"test-api-key",
onspring.WithBaseURL("http://[::1]:namedport"),
onspring.WithHTTPClient(client.HTTPClient()),
)
err := invalidClient.Ping.Get(t.Context())
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(t.Context())
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(t.Context())
if err == nil {
t.Errorf("Expected error, got nil")
}
})
})
}