From ce85b08fbc60cf92ac81b4a8aa6184c666838ebd Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:27:06 -0500 Subject: [PATCH] feat: implement field endpoints --- README.md | 114 +++++- apps.go | 15 +- apps_test.go | 15 +- client.go | 20 +- fields.go | 232 ++++++++++++ fields_test.go | 935 +++++++++++++++++++++++++++++++++++++++++++++++ pagingRequest.go | 13 + 7 files changed, 1324 insertions(+), 20 deletions(-) create mode 100644 fields.go create mode 100644 fields_test.go diff --git a/README.md b/README.md index a3d26c4..39c56ed 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ import ( client := onspring.NewClient("your-api-key") app, err := client.Apps.Get(context.TODO(), 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved App: %+v\n", app) +} ``` #### Get Apps by Page @@ -116,7 +122,13 @@ import ( client := onspring.NewClient("your-api-key") -page, err := client.Apps.List(t.Context()) +page, err := client.Apps.List(context.TODO()) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Page %d of Apps: %+v\n", page.PageNumber, page.Items) +} ``` ##### Retrieve all pages @@ -129,8 +141,12 @@ import ( client := onspring.NewClient("your-api-key") -for app, err := range client.Apps.ListAll(t.Context()) { - // Do stuff +for app, err := range client.Apps.ListAll(context.TODO()) { + if err != nil { + fmt.Printf("Error during iteration: %v\n", err) + break + } + fmt.Printf("Retrieved App: %+v\n", app) } ``` @@ -144,5 +160,95 @@ import ( client := onspring.NewClient("your-api-key") -batch, err := client.Apps.GetMany(t.Context(), []int{ 1 }) +batch, err := client.Apps.GetMany(context.TODO(), []int{ 1, 2 }) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved App Batch (Count: %d): %+v\n", batch.Count, batch.Items) +} ``` + +### Fields + +#### Get Field by Id + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +field, err := client.Fields.Get(context.TODO(), 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Field: %+v\n", field) +} +``` + +#### Get Fields by App + +##### Retrieve a single page + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +page, err := client.Fields.List(context.TODO(), 1) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Page %d of Fields: %+v\n", page.PageNumber, page.Items) +} +``` + +##### Retrieve all pages + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +for field, err := range client.Fields.ListAll(context.TODO(), 1) { + if err != nil { + fmt.Printf("Error during iteration: %v\n", err) + break + } + fmt.Printf("Retrieved Field: %+v\n", field) +} +``` + +#### Get Fields by Batch + +```go +import ( + "context" + "fmt" + "github.com/StevanFreeborn/onspring-api-sdk-go/onspring" +) + +client := onspring.NewClient("your-api-key") + +batch, err := client.Fields.GetMany(context.TODO(), []int{ 1, 2 }) + +if err != nil { + fmt.Printf("Error: %v\n", err) +} else { + fmt.Printf("Retrieved Field Batch (Count: %d): %+v\n", batch.Count, batch.Items) +} + diff --git a/apps.go b/apps.go index 1752db4..6ed4664 100644 --- a/apps.go +++ b/apps.go @@ -92,7 +92,7 @@ func (a *AppsEndpoint) ListAll(ctx context.Context, pagingOpts ...PagingOption) } } - if page.TotalPages == page.PageNumber { + if page.PageNumber >= page.TotalPages { break } @@ -156,16 +156,3 @@ func (a *AppsEndpoint) Get(ctx context.Context, id int) (App, error) { return app, nil } - -func createPagingRequest(pagingOpts []PagingOption) *PagingRequest { - pagingRequest := &PagingRequest{ - PageNumber: 1, - PageSize: 50, - } - - for _, opt := range pagingOpts { - opt(pagingRequest) - } - - return pagingRequest -} diff --git a/apps_test.go b/apps_test.go index c5af97d..3e56359 100644 --- a/apps_test.go +++ b/apps_test.go @@ -514,6 +514,8 @@ func TestApps(t *testing.T) { Items: apps, } + expectedIds := []int{apps[0].Id} + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("Expected POST method, got %s", r.Method) @@ -523,6 +525,17 @@ func TestApps(t *testing.T) { t.Errorf("Expected /apps/batch-get endpoint, got %s", r.URL.Path) } + var ids []int + err := json.NewDecoder(r.Body).Decode(&ids) + + if err != nil { + t.Errorf("Expected to decode request body, but got error: %v", err) + } + + if !slices.Equal(expectedIds, ids) { + t.Errorf("Expected body to be %v but got %v", expectedIds, ids) + } + jsonData, _ := json.Marshal(expectedBatch) w.WriteHeader(http.StatusOK) @@ -530,7 +543,7 @@ func TestApps(t *testing.T) { _, _ = w.Write(jsonData) }) - batch, _ := client.Apps.GetMany(t.Context(), []int{apps[0].Id}) + batch, _ := client.Apps.GetMany(t.Context(), expectedIds) if !reflect.DeepEqual(expectedBatch, batch) { t.Errorf("Expected %v but got %v", expectedBatch, batch) diff --git a/client.go b/client.go index 0206f3c..077a91d 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ package onspring import ( + "bytes" "context" "encoding/json" "fmt" @@ -44,6 +45,8 @@ type Client struct { Ping *PingEndpoint // Apps provides access to the apps within an Onspring instance. Apps *AppsEndpoint + // Fields provides access to the fields within an Onspring instance. + Fields *FieldsEndpoint } // NewClient creates a new Onspring API client with the provided API key. @@ -78,6 +81,7 @@ func NewClient(apiKey string, opts ...ClientOption) *Client { c.Ping = &PingEndpoint{client: c} c.Apps = &AppsEndpoint{client: c} + c.Fields = &FieldsEndpoint{client: c} return c } @@ -179,7 +183,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, queryParams map[string]string, _ any) (*http.Request, error) { +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") } @@ -201,6 +205,16 @@ func (c *Client) newRequest(ctx context.Context, method, path string, queryParam var bodyReader io.Reader + 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 { @@ -210,5 +224,9 @@ func (c *Client) newRequest(ctx context.Context, method, path string, queryParam req.Header.Set(defaultAPIKeyHeader, c.apiKey) req.Header.Set(defaultAPIVersionHeader, c.apiVersion) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + return req, nil } diff --git a/fields.go b/fields.go new file mode 100644 index 0000000..112a125 --- /dev/null +++ b/fields.go @@ -0,0 +1,232 @@ +package onspring + +import ( + "context" + "encoding/json" + "fmt" + "iter" + "net/http" +) + +const ( + fieldsPath = "/fields" +) + +// FieldsEndpoint provides access to fields in an Onspring instance. +type FieldsEndpoint struct { + client *Client +} + +// Field represents an Onspring field +type Field struct { + Id int `json:"id"` + AppId int `json:"appId"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + IsRequired bool `json:"isRequired"` + IsUnique bool `json:"isUnique"` + TypeData any `json:"-"` +} + +// FormulaField represents a formula field type in Onspring +type FormulaField struct { + OutputType string `json:"outputType"` + Values []string `json:"values"` +} + +// ReferenceField represents a reference field type in Onspring +type ReferenceField struct { + Multiplicity string `json:"multiplicity"` + ReferenceAppId string `json:"referenceAppId"` +} + +// ListField represents a list field type in Onspring +type ListField struct { + Multiplicity string `json:"multiplicity"` + Values []string `json:"values"` + ListId int `json:"listId"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Field. +// This allows for custom deserialization logic based on the 'Type' field. +func (f *Field) UnmarshalJSON(data []byte) error { + type Alias Field + + aux := &struct { + *Alias + }{ + Alias: (*Alias)(f), + } + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + switch f.Type { + case "Formula": + var formulaField FormulaField + + if err := json.Unmarshal(data, &formulaField); err != nil { + return err + } + + f.TypeData = formulaField + case "Reference": + var referenceField ReferenceField + + if err := json.Unmarshal(data, &referenceField); err != nil { + return err + } + + f.TypeData = referenceField + case "List": + var listField ListField + + if err := json.Unmarshal(data, &listField); err != nil { + return err + } + + f.TypeData = listField + default: + } + + return nil +} + +// FieldBatch represents a batch of Onspring fields +type FieldBatch struct { + Count int `json:"count"` + Items []Field `json:"items"` +} + +// Get retrieves a field from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - id: The id of the field to retrieve +// +// Returns: +// - Field: A field +// - error: An error if the request fails +func (f *FieldsEndpoint) Get(ctx context.Context, id int) (Field, error) { + path := fmt.Sprintf("%s/id/%d", fieldsPath, id) + req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, nil, nil) + + var field Field + + if requestCreationErr != nil { + return field, requestCreationErr + } + + responseErr := f.client.doWithJsonResponse(req, &field) + + if responseErr != nil { + return field, responseErr + } + + return field, nil +} + +// GetMany retrieves a batch of fields from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - ids: The ids of the fields to retrieve +// +// Returns: +// - FieldBatch: A batch of fields +// - error: An error if the request fails +func (f *FieldsEndpoint) GetMany(ctx context.Context, ids []int) (FieldBatch, error) { + path := fmt.Sprintf("%s/batch-get", fieldsPath) + req, requestCreationErr := f.client.newRequest(ctx, http.MethodPost, path, nil, ids) + + var fieldBatch FieldBatch + + if requestCreationErr != nil { + return fieldBatch, requestCreationErr + } + + responseErr := f.client.doWithJsonResponse(req, &fieldBatch) + + if responseErr != nil { + return fieldBatch, responseErr + } + + return fieldBatch, nil +} + +// List retrieves a paginated list of fields from the Onspring API. +// +// Parameters: +// - ctx: The context for the request +// - appId: The id of the app to retrieve fields for +// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize) +// +// Returns: +// - Page[Field]: A page of fields with pagination metadata +// - error: An error if the request fails +func (f *FieldsEndpoint) List(ctx context.Context, appId int, pagingOpts ...PagingOption) (Page[Field], error) { + pagingRequest := createPagingRequest(pagingOpts) + path := fmt.Sprintf("%s/appId/%d", fieldsPath, appId) + + req, requestCreationErr := f.client.newRequest(ctx, http.MethodGet, path, pagingRequest.ToParams(), nil) + + var page Page[Field] + + if requestCreationErr != nil { + return page, requestCreationErr + } + + responseErr := f.client.doWithJsonResponse(req, &page) + + if responseErr != nil { + return page, responseErr + } + + return page, nil +} + +// ListAll returns an iterator that yields all Fields for an app across all pages. +// It automatically handles pagination by making sequential calls to List +// until all items have been retrieved or the caller stops the iteration. +// +// The iterator yields each Field 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 +// - appId: The id of the app to retrieve fields for +// - pagingOpts: Optional paging configuration functions (e.g., ForPageNumber, WithPageSize) +// +// Returns: +// - iter.Seq2[Field, error]: An iterator yielding: +// - Field: The individual field record. +// - error: An error if a specific page request fails during iteration. +func (f *FieldsEndpoint) ListAll(ctx context.Context, appId int, pagingOpts ...PagingOption) iter.Seq2[Field, error] { + return func(yield func(Field, error) bool) { + pagingRequest := createPagingRequest(pagingOpts) + + for { + page, err := f.List(ctx, appId, ForPageNumber(pagingRequest.PageNumber), WithPageSize(pagingRequest.PageSize)) + + if err != nil { + yield(Field{}, err) + return + } + + for _, item := range page.Items { + if !yield(item, nil) { + return + } + } + + if page.PageNumber >= page.TotalPages { + break + } + + pagingRequest.PageNumber++ + } + } +} diff --git a/fields_test.go b/fields_test.go new file mode 100644 index 0000000..52bbb0f --- /dev/null +++ b/fields_test.go @@ -0,0 +1,935 @@ +package onspring_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "reflect" + "slices" + "strconv" + "testing" + + "github.com/StevanFreeborn/onspring-api-sdk-go" +) + +func TestFields(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 + + _, err := client.Fields.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.Fields.List(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.Fields.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.Fields.Get(t.Context(), 0) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the /fields/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.Fields.Get(t.Context(), 0) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should return a field if the /fields/id/:id endpoint returns a 200 status code", func(t *testing.T) { + expectedField := onspring.Field{ + Id: 1, + AppId: 1, + Name: "Field", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + } + + _, 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("/fields/id/%d", expectedField.Id) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + jsonData, _ := json.Marshal(expectedField) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + field, _ := client.Fields.Get(t.Context(), expectedField.Id) + + if !reflect.DeepEqual(expectedField, field) { + t.Errorf("Expected %v but got %v", expectedField, field) + } + }) + + }) + + t.Run("UnmarshalJSON", func(t *testing.T) { + t.Run("it should unmarshal a Formula field correctly", func(t *testing.T) { + jsonStr := `{ + "id": 1, + "appId": 10, + "name": "Calc Field", + "type": "Formula", + "status": "Enabled", + "isRequired": true, + "isUnique": false, + "outputType": "Number", + "values": ["1", "2", "3"] + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if field.Type != "Formula" { + t.Errorf("Expected Type 'Formula', got %s", field.Type) + } + + formulaField, ok := field.TypeData.(onspring.FormulaField) + + if !ok { + t.Fatalf("Expected TypeData to be FormulaField, got %T", field.TypeData) + } + + expectedFormulaField := onspring.FormulaField{ + OutputType: "Number", + Values: []string{"1", "2", "3"}, + } + + if !reflect.DeepEqual(formulaField, expectedFormulaField) { + t.Errorf("Expected FormulaField %+v, got %+v", expectedFormulaField, formulaField) + } + }) + + t.Run("it should unmarshal a Reference field correctly", func(t *testing.T) { + jsonStr := `{ + "id": 2, + "appId": 20, + "name": "Ref Field", + "type": "Reference", + "status": "Enabled", + "isRequired": false, + "isUnique": true, + "multiplicity": "OneToOne", + "referenceAppId": "123" + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if field.Type != "Reference" { + t.Errorf("Expected Type 'Reference', got %s", field.Type) + } + + referenceField, ok := field.TypeData.(onspring.ReferenceField) + + if !ok { + t.Fatalf("Expected TypeData to be ReferenceField, got %T", field.TypeData) + } + + expectedReferenceField := onspring.ReferenceField{ + Multiplicity: "OneToOne", + ReferenceAppId: "123", + } + + if !reflect.DeepEqual(referenceField, expectedReferenceField) { + t.Errorf("Expected ReferenceField %+v, got %+v", expectedReferenceField, referenceField) + } + }) + + t.Run("it should unmarshal a List field correctly", func(t *testing.T) { + jsonStr := `{ + "id": 3, + "appId": 30, + "name": "List Field", + "type": "List", + "status": "Enabled", + "isRequired": false, + "isUnique": false, + "multiplicity": "MultiSelect", + "values": ["OptionA", "OptionB"], + "listId": 456 + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if field.Type != "List" { + t.Errorf("Expected Type 'List', got %s", field.Type) + } + + listField, ok := field.TypeData.(onspring.ListField) + + if !ok { + t.Fatalf("Expected TypeData to be ListField, got %T", field.TypeData) + } + + expectedListField := onspring.ListField{ + Multiplicity: "MultiSelect", + Values: []string{"OptionA", "OptionB"}, + ListId: 456, + } + + if !reflect.DeepEqual(listField, expectedListField) { + t.Errorf("Expected ListField %+v, got %+v", expectedListField, listField) + } + }) + + t.Run("it should handle unknown field types without TypeData", func(t *testing.T) { + jsonStr := `{ + "id": 4, + "appId": 40, + "name": "Text Field", + "type": "Text", + "status": "Enabled", + "isRequired": true, + "isUnique": false + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if field.Type != "Text" { + t.Errorf("Expected Type 'Text', got %s", field.Type) + } + + if field.TypeData != nil { + t.Errorf("Expected TypeData to be nil for 'Text' type, got %+v", field.TypeData) + } + }) + + t.Run("it should return an error for malformed JSON for main field struct", func(t *testing.T) { + jsonStr := `{ + "id": "invalid", + "appId": 10, + "name": "Calc Field", + "type": "Formula" + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err == nil { + t.Fatalf("Expected UnmarshalJSON to fail for malformed main struct JSON, got nil") + } + }) + + t.Run("it should return an error for malformed JSON for TypeData (Formula)", func(t *testing.T) { + jsonStr := `{ + "id": 1, + "appId": 10, + "name": "Calc Field", + "type": "Formula", + "outputType": 123, + "values": ["1", "2", "3"] + }` + + var field onspring.Field + + err := json.Unmarshal([]byte(jsonStr), &field) + + if err == nil { + t.Fatalf("Expected UnmarshalJSON to fail for malformed Formula TypeData JSON, got nil") + } + }) + }) + + 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.Fields.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.Fields.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.Fields.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.Fields.GetMany(t.Context(), []int{}) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return an error if the /fields/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.Fields.GetMany(t.Context(), []int{}) + + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("it should return a batch of fields when the /fields/batch-get endpoint returns a 200 status code", func(t *testing.T) { + fields := []onspring.Field{ + { + Id: 1, + AppId: 1, + Name: "Field 1", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + } + + expectedBatch := onspring.FieldBatch{ + Count: len(fields), + Items: fields, + } + + expectedIds := []int{fields[0].Id} + + _, 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 != "/fields/batch-get" { + t.Errorf("Expected /fields/batch-get endpoint, got %s", r.URL.Path) + } + + var ids []int + err := json.NewDecoder(r.Body).Decode(&ids) + + if err != nil { + t.Errorf("Expected to decode request body, but got error: %v", err) + } + + if !slices.Equal(expectedIds, ids) { + t.Errorf("Expected body to be %v but got %v", expectedIds, ids) + } + + jsonData, _ := json.Marshal(expectedBatch) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + }) + + batch, _ := client.Fields.GetMany(t.Context(), expectedIds) + + if !reflect.DeepEqual(expectedBatch, batch) { + t.Errorf("Expected %v but got %v", expectedBatch, batch) + } + }) + }) + + 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.Fields.List(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.Fields.List(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.Fields.List(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.Fields.List(t.Context(), 0) + + if err == nil { + t.Errorf("Expected request creation error, got nil") + } + }) + + t.Run("it should return a page of fields if the /fields/appId/:appId endpoint returns a 200 status code", func(t *testing.T) { + appId := 1 + expectedPageNumber := 1 + expectedPageSize := 50 + + expectedPage := onspring.Page[onspring.Field]{ + TotalPages: 1, + TotalRecords: 1, + PageNumber: 1, + PageSize: 50, + Items: []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + }, + } + + _, 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("/fields/appId/%d", appId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, 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.Fields.List(t.Context(), appId) + + 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 /fields/appId/:appId endpoint with non-default paging information when provided", func(t *testing.T) { + appId := 1 + expectedPageNumber := 2 + expectedPageSize := 1 + + expectedPage := onspring.Page[onspring.Field]{ + TotalPages: 1, + TotalRecords: 1, + PageNumber: 1, + PageSize: 1, + Items: []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + }, + } + + _, 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("/fields/appId/%d", appId) + + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, 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) + }) + + _, _ = client.Fields.List( + t.Context(), + appId, + onspring.ForPageNumber(expectedPageNumber), + onspring.WithPageSize(expectedPageSize), + ) + }) + + t.Run("it should return an error if the /fields/appId/:appId 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.Fields.List(t.Context(), 0) + + 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 fields", func(t *testing.T) { + _, client := setupMockServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + + for _, err := range client.Fields.ListAll(t.Context(), 0) { + if err == nil { + t.Errorf("Expected error, got nil") + } + } + }) + + t.Run("it should return all the fields from multiple pages", func(t *testing.T) { + appId := 1 + expectedFields := []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field 1", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + { + Id: 2, + AppId: appId, + Name: "Field 2", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + } + + pageOne := onspring.Page[onspring.Field]{ + TotalPages: 2, + TotalRecords: 2, + PageNumber: 1, + PageSize: 1, + Items: []onspring.Field{expectedFields[0]}, + } + + pageTwo := onspring.Page[onspring.Field]{ + TotalPages: 2, + TotalRecords: 2, + PageNumber: 2, + PageSize: 1, + Items: []onspring.Field{expectedFields[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) + } + + expectedPath := fmt.Sprintf("/fields/appId/%d", appId) + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, 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) + } + }) + + retrievedFields := []onspring.Field{} + + for field, _ := range client.Fields.ListAll(t.Context(), appId) { + retrievedFields = append(retrievedFields, field) + } + + if !slices.Equal(expectedFields, retrievedFields) { + t.Errorf("Expected %v but got %v", expectedFields, retrievedFields) + } + }) + + t.Run("it should return fields and errors if some pages fail and some succeed", func(t *testing.T) { + appId := 1 + expectedFields := []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field 1", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + { + Id: 2, + AppId: appId, + Name: "Field 2", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + } + + page := onspring.Page[onspring.Field]{ + TotalPages: 2, + TotalRecords: 2, + PageNumber: 1, + PageSize: 1, + Items: expectedFields, + } + + _, 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("/fields/appId/%d", appId) + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + pageNumber := r.URL.Query().Get("pageNumber") + + if pageNumber == "1" { + jsonData, _ := json.Marshal(page) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + } + + if pageNumber == "2" { + w.WriteHeader(http.StatusInternalServerError) + } + }) + + retrievedFields := []onspring.Field{} + encounteredErrors := []error{} + + for field, err := range client.Fields.ListAll(t.Context(), appId) { + if err != nil { + encounteredErrors = append(encounteredErrors, err) + } else { + retrievedFields = append(retrievedFields, field) + } + } + + if !slices.Equal(expectedFields, retrievedFields) { + t.Errorf("Expected %v but got %v", expectedFields, retrievedFields) + } + + 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) { + appId := 1 + expectedFields := []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field 1", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + { + Id: 2, + AppId: appId, + Name: "Field 2", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + } + + pageTwo := onspring.Page[onspring.Field]{ + TotalPages: 2, + TotalRecords: 2, + PageNumber: 2, + PageSize: 1, + Items: expectedFields, + } + + _, 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("/fields/appId/%d", appId) + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, 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) + } + }) + + retrievedFields := []onspring.Field{} + + for field, _ := range client.Fields.ListAll(t.Context(), appId, onspring.ForPageNumber(2)) { + retrievedFields = append(retrievedFields, field) + } + + if !slices.Equal(expectedFields, retrievedFields) { + t.Errorf("Expected %v but got %v", expectedFields, retrievedFields) + } + }) + + t.Run("it should retrieve pages using specified page size when given", func(t *testing.T) { + appId := 1 + expectedFields := []onspring.Field{ + { + Id: 1, + AppId: appId, + Name: "Field 1", + Type: "Text", + Status: "Enabled", + IsRequired: true, + IsUnique: false, + }, + } + + page := onspring.Page[onspring.Field]{ + TotalPages: 1, + TotalRecords: 1, + PageNumber: 1, + PageSize: 1, + Items: expectedFields, + } + + _, 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("/fields/appId/%d", appId) + if r.URL.Path != expectedPath { + t.Errorf("Expected %s endpoint, got %s", expectedPath, r.URL.Path) + } + + pageSize := r.URL.Query().Get("pageSize") + + if pageSize == "1" { + jsonData, _ := json.Marshal(page) + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(jsonData) + } + }) + + retrievedFields := []onspring.Field{} + + for field, _ := range client.Fields.ListAll(t.Context(), appId, onspring.WithPageSize(1)) { + retrievedFields = append(retrievedFields, field) + } + + if !slices.Equal(expectedFields, retrievedFields) { + t.Errorf("Expected %v but got %v", expectedFields, retrievedFields) + } + }) + }) +} diff --git a/pagingRequest.go b/pagingRequest.go index cd45b9d..b540ab1 100644 --- a/pagingRequest.go +++ b/pagingRequest.go @@ -34,3 +34,16 @@ func WithPageSize(pageSize int) PagingOption { pr.PageSize = pageSize } } + +func createPagingRequest(pagingOpts []PagingOption) *PagingRequest { + pagingRequest := &PagingRequest{ + PageNumber: 1, + PageSize: 50, + } + + for _, opt := range pagingOpts { + opt(pagingRequest) + } + + return pagingRequest +}