Skip to content

Commit 28eb0d0

Browse files
committed
Consolidate the various approaches for making custom calls to the Genesys Cloud API apart from the SDK Client into a single Custom API Client library.
1 parent 16fd35b commit 28eb0d0

23 files changed

+874
-1374
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,57 @@ $ make testunit
170170

171171
If you want to go off of an example, we recommend using the [external contacts](https://github.com/MyPureCloud/terraform-provider-genesyscloud/tree/main/genesyscloud/external_contacts) package.
172172

173+
### Custom API Client
174+
175+
When a proxy function needs to call a Genesys Cloud API endpoint that doesn't have a generated SDK method, use the `custom_api_client` package (`genesyscloud/custom_api_client`) instead of calling `APIClient.CallAPI()` directly or constructing raw `http.Client` requests. This package handles authorization headers, content-type negotiation, query parameter encoding, error handling, and SDK debug logging context automatically.
176+
177+
```go
178+
import customapi "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/custom_api_client"
179+
```
180+
181+
**Available functions:**
182+
183+
| Function | Use case |
184+
|----------|----------|
185+
| `Do[T]` | Typed JSON response — unmarshals into `T` |
186+
| `DoNoResponse` | No response body (DELETE, PUT with no return) |
187+
| `DoRaw` | Raw `[]byte` response for custom unmarshaling |
188+
| `DoWithAcceptHeader` | Non-JSON responses (e.g., `text/csv`) |
189+
190+
**Example — adding to a proxy struct:**
191+
192+
```go
193+
type myProxy struct {
194+
clientConfig *platformclientv2.Configuration
195+
myApi *platformclientv2.MyApi
196+
customApiClient *customapi.Client
197+
}
198+
199+
func newMyProxy(clientConfig *platformclientv2.Configuration) *myProxy {
200+
return &myProxy{
201+
clientConfig: clientConfig,
202+
myApi: platformclientv2.NewMyApiWithConfig(clientConfig),
203+
customApiClient: customapi.NewClient(clientConfig, ResourceType),
204+
}
205+
}
206+
```
207+
208+
**Example — making a call:**
209+
210+
```go
211+
// Typed response
212+
result, resp, err := customapi.Do[platformclientv2.MyEntity](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/my/endpoint", nil, nil)
213+
214+
// With query params
215+
qp := customapi.NewQueryParams(map[string]string{"pageSize": "100", "pageNumber": "1"})
216+
result, resp, err := customapi.Do[platformclientv2.MyListing](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/my/endpoint", nil, qp)
217+
218+
// Delete with no response
219+
resp, err := customapi.DoNoResponse(ctx, p.customApiClient, customapi.MethodDelete, "/api/v2/my/endpoint/"+id, nil, nil)
220+
```
221+
222+
See the [package documentation](genesyscloud/custom_api_client/custom_api_client.go) for full details and additional examples.
223+
173224
### Documentation Generation
174225

175226
The provider documentation is automatically generated from resource schemas and example files. Understanding this process is crucial when adding new resources.

genesyscloud/architect_datatable/resource_genesyscloud_architect_datatable_proxy.go

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package architect_datatable
22

33
import (
44
"context"
5-
"encoding/json"
6-
"errors"
7-
"net/http"
85

6+
customapi "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/custom_api_client"
97
"github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/provider"
108

119
"github.com/mypurecloud/platform-client-sdk-go/v179/platformclientv2"
@@ -23,6 +21,7 @@ type getAllArchitectDatatableFunc func(ctx context.Context, p *architectDatatabl
2321
type architectDatatableProxy struct {
2422
clientConfig *platformclientv2.Configuration
2523
architectApi *platformclientv2.ArchitectApi
24+
customApiClient *customapi.Client
2625
createOrUpdateArchitectDatatableAttr createOrUpdateArchitectDatatableFunc
2726
getArchitectDatatableAttr getArchitectDatatableFunc
2827
getAllArchitectDatatableAttr getAllArchitectDatatableFunc
@@ -34,6 +33,7 @@ func newArchitectDatatableProxy(clientConfig *platformclientv2.Configuration) *a
3433
return &architectDatatableProxy{
3534
clientConfig: clientConfig,
3635
architectApi: api,
36+
customApiClient: customapi.NewClient(clientConfig, ResourceType),
3737
createOrUpdateArchitectDatatableAttr: createOrUpdateArchitectDatatableFn,
3838
getArchitectDatatableAttr: getArchitectDatatableFn,
3939
getAllArchitectDatatableAttr: getAllArchitectDatatableFn,
@@ -73,78 +73,24 @@ func createOrUpdateArchitectDatatableFn(ctx context.Context, p *architectDatatab
7373
// Set resource context for SDK debug logging
7474
ctx = provider.EnsureResourceContext(ctx, ResourceType)
7575

76-
apiClient := &p.architectApi.Configuration.APIClient
77-
action := http.MethodPost
78-
79-
// create path and map variables
80-
path := p.architectApi.Configuration.BasePath + "/api/v2/flows/datatables"
76+
method := customapi.MethodPost
77+
path := "/api/v2/flows/datatables"
8178

8279
if !createAction {
83-
action = http.MethodPut
80+
method = customapi.MethodPut
8481
path += "/" + *datatable.Id
8582
}
8683

87-
headerParams := make(map[string]string)
88-
89-
// add default headers if any
90-
for key := range p.architectApi.Configuration.DefaultHeader {
91-
headerParams[key] = p.architectApi.Configuration.DefaultHeader[key]
92-
}
93-
94-
headerParams["Authorization"] = "Bearer " + p.architectApi.Configuration.AccessToken
95-
headerParams["Content-Type"] = "application/json"
96-
headerParams["Accept"] = "application/json"
97-
98-
var successPayload *Datatable
99-
response, err := apiClient.CallAPI(path, action, datatable, headerParams, nil, nil, "", nil, "")
100-
101-
if err != nil {
102-
// Nothing special to do here, but do avoid processing the response
103-
} else if response.Error != nil {
104-
err = errors.New(response.ErrorMessage)
105-
} else {
106-
err = json.Unmarshal([]byte(response.RawBody), &successPayload)
107-
}
108-
109-
return successPayload, response, err
84+
return customapi.Do[Datatable](ctx, p.customApiClient, method, path, datatable, nil)
11085
}
11186

11287
func getArchitectDatatableFn(ctx context.Context, p *architectDatatableProxy, datatableId string, expanded string) (*Datatable, *platformclientv2.APIResponse, error) {
11388
// Set resource context for SDK debug logging
11489
ctx = provider.EnsureResourceContext(ctx, ResourceType)
11590

116-
apiClient := &p.architectApi.Configuration.APIClient
91+
queryParams := customapi.NewQueryParams(map[string]string{"expand": expanded})
11792

118-
// create path and map variables
119-
path := p.architectApi.Configuration.BasePath + "/api/v2/flows/datatables/" + datatableId
120-
121-
headerParams := make(map[string]string)
122-
queryParams := make(map[string]string)
123-
124-
// oauth required
125-
if p.architectApi.Configuration.AccessToken != "" {
126-
headerParams["Authorization"] = "Bearer " + p.architectApi.Configuration.AccessToken
127-
}
128-
// add default headers if any
129-
for key := range p.architectApi.Configuration.DefaultHeader {
130-
headerParams[key] = p.architectApi.Configuration.DefaultHeader[key]
131-
}
132-
133-
queryParams["expand"] = apiClient.ParameterToString(expanded, "")
134-
135-
headerParams["Content-Type"] = "application/json"
136-
headerParams["Accept"] = "application/json"
137-
138-
var successPayload *Datatable
139-
response, err := apiClient.CallAPI(path, http.MethodGet, nil, headerParams, queryParams, nil, "", nil, "")
140-
if err != nil {
141-
// Nothing special to do here, but do avoid processing the response
142-
} else if response.Error != nil {
143-
err = errors.New(response.ErrorMessage)
144-
} else {
145-
err = json.Unmarshal(response.RawBody, &successPayload)
146-
}
147-
return successPayload, response, err
93+
return customapi.Do[Datatable](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/flows/datatables/"+datatableId, nil, queryParams)
14894
}
14995

15096
func deleteArchitectDatatableFn(ctx context.Context, p *architectDatatableProxy, datatableId string) (*platformclientv2.APIResponse, error) {

genesyscloud/architect_datatable_row/resource_genesyscloud_architect_datatable_row_proxy.go

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ package architect_datatable_row
22

33
import (
44
"context"
5-
"encoding/json"
6-
"errors"
75
"log"
8-
"net/http"
96

107
"github.com/mitchellh/mapstructure"
8+
customapi "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/custom_api_client"
119
"github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/provider"
1210
rc "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/resource_cache"
1311

@@ -26,6 +24,7 @@ type deleteArchitectDatatableRowFunc func(ctx context.Context, p *architectDatat
2624
type architectDatatableRowProxy struct {
2725
clientConfig *platformclientv2.Configuration
2826
architectApi *platformclientv2.ArchitectApi
27+
customApiClient *customapi.Client
2928
createArchitectDatatableRowAttr createArchitectDatatableRowFunc
3029
getArchitectDatatableAttr getArchitectDatatableFunc
3130
getAllArchitectDatatableAttr getAllArchitectDatatableFunc
@@ -47,6 +46,7 @@ func newArchitectDatatableRowProxy(clientConfig *platformclientv2.Configuration)
4746
return &architectDatatableRowProxy{
4847
clientConfig: clientConfig,
4948
architectApi: api,
49+
customApiClient: customapi.NewClient(clientConfig, ResourceType),
5050
dataTableRowCache: dataTableRowCache,
5151
dataTableCache: dataTableCache,
5252
getArchitectDatatableAttr: getArchitectDatatableFn,
@@ -149,38 +149,9 @@ func getArchitectDatatableFn(ctx context.Context, p *architectDatatableRowProxy,
149149
return eg, nil, nil
150150
}
151151

152-
apiClient := &p.architectApi.Configuration.APIClient
152+
queryParams := customapi.NewQueryParams(map[string]string{"expand": expanded})
153153

154-
// create path and map variables
155-
path := p.architectApi.Configuration.BasePath + "/api/v2/flows/datatables/" + datatableId
156-
157-
headerParams := make(map[string]string)
158-
queryParams := make(map[string]string)
159-
160-
// oauth required
161-
if p.architectApi.Configuration.AccessToken != "" {
162-
headerParams["Authorization"] = "Bearer " + p.architectApi.Configuration.AccessToken
163-
}
164-
// add default headers if any
165-
for key := range p.architectApi.Configuration.DefaultHeader {
166-
headerParams[key] = p.architectApi.Configuration.DefaultHeader[key]
167-
}
168-
169-
queryParams["expand"] = apiClient.ParameterToString(expanded, "")
170-
171-
headerParams["Content-Type"] = "application/json"
172-
headerParams["Accept"] = "application/json"
173-
174-
var successPayload *Datatable
175-
response, err := apiClient.CallAPI(path, http.MethodGet, nil, headerParams, queryParams, nil, "", nil, "")
176-
if err != nil {
177-
// Nothing special to do here, but do avoid processing the response
178-
} else if response.Error != nil {
179-
err = errors.New(response.ErrorMessage)
180-
} else {
181-
err = json.Unmarshal(response.RawBody, &successPayload)
182-
}
183-
return successPayload, response, err
154+
return customapi.Do[Datatable](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/flows/datatables/"+datatableId, nil, queryParams)
184155
}
185156

186157
func getAllArchitectDatatableRowsFn(ctx context.Context, p *architectDatatableRowProxy, tableId string) (*[]map[string]interface{}, *platformclientv2.APIResponse, error) {

genesyscloud/architect_flow/resource_genesyscloud_architect_flow_proxy.go

Lines changed: 17 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package architect_flow
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
7-
"io"
86
"log"
97
"net/http"
10-
"net/url"
118
"time"
129

10+
customapi "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/custom_api_client"
1311
"github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/provider"
1412
rc "github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/resource_cache"
1513
"github.com/mypurecloud/terraform-provider-genesyscloud/genesyscloud/util"
@@ -33,8 +31,9 @@ type getExportJobStatusByIdFunc func(a *architectFlowProxy, jobId string) (*plat
3331
type pollExportJobForDownloadUrlFunc func(a *architectFlowProxy, jobId string, timeoutInSeconds float64) (downloadUrl string, err error)
3432

3533
type architectFlowProxy struct {
36-
clientConfig *platformclientv2.Configuration
37-
api *platformclientv2.ArchitectApi
34+
clientConfig *platformclientv2.Configuration
35+
customApiClient *customapi.Client
36+
api *platformclientv2.ArchitectApi
3837

3938
getArchitectFlowAttr getArchitectFunc
4039
getAllArchitectFlowsAttr getAllArchitectFlowsFunc
@@ -56,8 +55,9 @@ var flowCache = rc.NewResourceCache[platformclientv2.Flow]()
5655
func newArchitectFlowProxy(clientConfig *platformclientv2.Configuration) *architectFlowProxy {
5756
api := platformclientv2.NewArchitectApiWithConfig(clientConfig)
5857
return &architectFlowProxy{
59-
clientConfig: clientConfig,
60-
api: api,
58+
clientConfig: clientConfig,
59+
customApiClient: customapi.NewClient(clientConfig, ResourceType),
60+
api: api,
6161

6262
getArchitectFlowAttr: getArchitectFlowFn,
6363
getAllArchitectFlowsAttr: getAllArchitectFlowsFn,
@@ -322,49 +322,35 @@ func getArchitectFlowJobsFn(ctx context.Context, p *architectFlowProxy, jobId st
322322

323323
// getAllArchitectFlowsFn is the implementation function for GetAllFlows
324324
func getAllArchitectFlowsFn(ctx context.Context, p *architectFlowProxy, name string, varType []string) (*[]platformclientv2.Flow, *platformclientv2.APIResponse, error) {
325-
baseURL := p.clientConfig.BasePath + "/api/v2/flows"
326325
ctx = provider.EnsureResourceContext(ctx, ResourceType)
326+
var allFlows []platformclientv2.Flow
327327

328-
params := url.Values{}
328+
queryParams := customapi.QueryParams{}
329+
queryParams.Set("pageSize", "100")
330+
queryParams.Set("pageNumber", "1")
331+
queryParams.Set("includeSchemas", "true")
329332
if name != "" {
330-
params.Add("name", name)
333+
queryParams.Set("name", name)
331334
}
332335
for _, t := range varType {
333-
params.Add("type", t)
334-
}
335-
params.Add("includeSchemas", "true")
336-
337-
client := &http.Client{}
338-
var allFlows []platformclientv2.Flow
339-
340-
params.Set("pageSize", "100")
341-
params.Set("pageNumber", "1")
342-
343-
u, err := url.Parse(baseURL)
344-
if err != nil {
345-
return nil, nil, fmt.Errorf("error parsing URL: %v", err)
336+
queryParams.Add("type", t)
346337
}
347-
u.RawQuery = params.Encode()
348338

349-
flows, apiResp, err := makeFlowRequest(ctx, client, u.String(), p)
339+
flows, apiResp, err := customapi.Do[platformclientv2.Flowentitylisting](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/flows", nil, queryParams)
350340
if err != nil {
351341
return nil, apiResp, err
352342
}
353-
354343
if flows.Entities != nil {
355344
allFlows = append(allFlows, *flows.Entities...)
356345
}
357346

358347
for pageNum := 2; pageNum <= *flows.PageCount; pageNum++ {
359348
ctx = provider.EnsureResourceContext(ctx, ResourceType)
360-
params.Set("pageNumber", fmt.Sprintf("%d", pageNum))
361-
u.RawQuery = params.Encode()
362-
363-
pageFlows, _, err := makeFlowRequest(ctx, client, u.String(), p)
349+
queryParams.Set("pageNumber", fmt.Sprintf("%d", pageNum))
350+
pageFlows, _, err := customapi.Do[platformclientv2.Flowentitylisting](ctx, p.customApiClient, customapi.MethodGet, "/api/v2/flows", nil, queryParams)
364351
if err != nil {
365352
return nil, apiResp, err
366353
}
367-
368354
if pageFlows.Entities != nil {
369355
allFlows = append(allFlows, *pageFlows.Entities...)
370356
}
@@ -373,50 +359,9 @@ func getAllArchitectFlowsFn(ctx context.Context, p *architectFlowProxy, name str
373359
for _, flow := range allFlows {
374360
rc.SetCache(p.flowCache, *flow.Id, flow)
375361
}
376-
377362
return &allFlows, apiResp, nil
378363
}
379364

380-
func makeFlowRequest(ctx context.Context, client *http.Client, url string, p *architectFlowProxy) (*platformclientv2.Flowentitylisting, *platformclientv2.APIResponse, error) {
381-
// Set resource context for SDK debug logging before creating HTTP request
382-
ctx = provider.EnsureResourceContext(ctx, ResourceType)
383-
384-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
385-
if err != nil {
386-
return nil, nil, err
387-
}
388-
389-
req.Header.Set("Authorization", "Bearer "+p.clientConfig.AccessToken)
390-
req.Header.Set("Content-Type", "application/json")
391-
392-
resp, err := client.Do(req)
393-
if err != nil {
394-
return nil, nil, fmt.Errorf("error making request: %v", err)
395-
}
396-
defer resp.Body.Close()
397-
398-
respBody, err := io.ReadAll(resp.Body)
399-
if err != nil {
400-
return nil, nil, fmt.Errorf("error reading response: %v", err)
401-
}
402-
403-
apiResp := &platformclientv2.APIResponse{
404-
StatusCode: resp.StatusCode,
405-
Response: resp,
406-
}
407-
408-
if resp.StatusCode >= 400 {
409-
return nil, apiResp, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(respBody))
410-
}
411-
412-
var flows platformclientv2.Flowentitylisting
413-
if err := json.Unmarshal(respBody, &flows); err != nil {
414-
return nil, apiResp, err
415-
}
416-
417-
return &flows, apiResp, nil
418-
}
419-
420365
// generateDownloadUrlFn is the implementation function for the generateDownloadUrl method
421366
func generateDownloadUrlFn(a *architectFlowProxy, flowId string) (downloadUrl string, err error) {
422367
defer func() {

0 commit comments

Comments
 (0)