From 3ef87046f0f5c888c7fa163a3538730c010e5706 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Wed, 19 Nov 2025 10:12:16 -0500 Subject: [PATCH 1/7] feat: add APIExecutor for raw HTTP calls to OpenFGA endpoints --- CHANGELOG.md | 3 + README.md | 83 + api_client.go | 6 + api_executor.go | 455 ++++++ api_open_fga.go | 2771 ++++------------------------------ example/example1/example1.go | 52 +- utils.go | 14 + 7 files changed, 868 insertions(+), 2516 deletions(-) create mode 100644 api_executor.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e982d..aff600c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # Changelog ## [Unreleased](https://github.com/openfga/go-sdk/compare/v0.7.3...HEAD) + +- feat: add a generic API Executor `fgaClient.GetAPIExecutor()` to allow calling any OpenFGA API method. See [Calling Other Endpoints](./README.md#calling-other-endpoints) for more. - feat: add generic `ToPtr[T any](v T) *T` function for creating pointers to any type - deprecation: `PtrBool`, `PtrInt`, `PtrInt32`, `PtrInt64`, `PtrFloat32`, `PtrFloat64`, `PtrString`, and `PtrTime` are now deprecated in favor of the generic `ToPtr` function - feat: add a top-level makefile in go-sdk to simplify running tests and linters: (#250) - feat: add support for StreamedListObjects endpoint (#252) + ## v0.7.3 ### [0.7.3](https://github.com/openfga/go-sdk/compare/v0.7.2...v0.7.3) (2025-10-08) diff --git a/README.md b/README.md index 103f257..f9eb47a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ This is an autogenerated Go SDK for OpenFGA. It provides a wrapper around the [O - [Assertions](#assertions) - [Read Assertions](#read-assertions) - [Write Assertions](#write-assertions) + - [Calling other endpoints](#calling-other-endpoints) - [Retries](#retries) - [API Endpoints](#api-endpoints) - [Models](#models) @@ -1092,7 +1093,89 @@ data, err := fgaClient.WriteAssertions(context.Background()). Execute() ``` +### Calling Other Endpoints +In certain cases you may want to call other APIs not yet wrapped by the SDK. You can do so by using the `APIExecutor` available from the `fgaClient`. +The `APIExecutor` allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the operation name, HTTP method, path, parameters, body, and headers, while still taking into account the configuration and handling authentication, telemetry, retry and error handling. + +This is useful when: + +- you want to call a new endpoint that is not yet supported by the SDK +- you are using an earlier version of the SDK that doesn't yet support a particular endpoint +- you have a custom endpoint deployed that extends the OpenFGA API + +In all cases, you initialize the SDK the same way as usual, and then get the `APIExecutor` from the `fgaClient` instance. + +```go +// Initialize the client, same as above +fgaClient, err := NewSdkClient(&ClientConfiguration{...}) + +// Get the generic API executor +executor := fgaClient.GetAPIExecutor() + +// Custom new endpoint that doesn't exist in the SDK yet +requestBody := map[string]interface{}{ + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", +} + +// Build the request +request := openfga.NewAPIExecutorRequestBuilder("CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint"). + WithPathParameter("store_id", storeID). + WithQueryParameter("page_size", "20"). + WithQueryParameter("continuation_token", "eyJwayI6..."). + WithBody(requestBody). + WithHeader("X-Experimental-Feature", "enabled"). + Build() +``` + +#### Example: Calling a new "Custom Endpoint" endpoint and handling raw response + +```go +// Get raw response without automatic decoding +rawResponse, err := executor.Execute(ctx, request) + +if err != nil { + log.Fatalf("Custom endpoint failed: %v", err) +} + +// Manually decode the response +var result map[string]interface{} +if err := json.Unmarshal(rawResponse.Body, &result); err != nil { + log.Fatalf("Failed to decode: %v", err) +} + +fmt.Printf("Response: %+v\n", result) + +// You can access fields like headers, status code, etc. from rawResponse: +fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) +fmt.Printf("Headers: %+v\n", rawResponse.Headers) +``` + +#### Example: Calling a new "Custom Endpoint" endpoint and decoding response into a struct + +```go +// Define a struct to hold the response +type CustomEndpointResponse struct { + Allowed bool `json:"allowed"` + Reason string `json:"reason"` +} +var customEndpointResponse CustomEndpointResponse + +// Get raw response decoded into CustomEndpointResponse struct +rawResponse, err := executor.Execute(ctx, request, &customEndpointResponse) // Pass pointer to struct for decoding + +if err != nil { + log.Fatalf("Custom endpoint failed: %v", err) +} + +fmt.Printf("Response: %+v\n", customEndpointResponse) + +// You can access fields like headers, status code, etc. from rawResponse: +fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) +fmt.Printf("Headers: %+v\n", rawResponse.Headers) +```` ### Retries diff --git a/api_client.go b/api_client.go index 51b891f..b4377c0 100644 --- a/api_client.go +++ b/api_client.go @@ -214,6 +214,12 @@ func (c *APIClient) GetConfig() *Configuration { return c.cfg } +// GetAPIExecutor returns an APIExecutor that can be used to call any OpenFGA API endpoint +// This is useful for calling endpoints that are not yet supported by the SDK +func (c *APIClient) GetAPIExecutor() APIExecutor { + return NewAPIExecutor(c) +} + // prepareRequest build the request func (c *APIClient) prepareRequest( ctx context.Context, diff --git a/api_executor.go b/api_executor.go new file mode 100644 index 0000000..a2a2b64 --- /dev/null +++ b/api_executor.go @@ -0,0 +1,455 @@ +package openfga + +import ( + "bytes" + "context" + "errors" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/openfga/go-sdk/internal/constants" + "github.com/openfga/go-sdk/internal/utils/retryutils" + "github.com/openfga/go-sdk/telemetry" +) + +// APIExecutorRequest represents a request to be executed by the API executor. +type APIExecutorRequest struct { + // OperationName is a descriptive name for the operation (e.g., "Check", "Write", "BatchCheck"). + // Used for logging, telemetry, and error messages. + OperationName string + + // Method is the HTTP method (GET, POST, PUT, DELETE, etc.). + Method string + + // Path is the API path (e.g., "/stores/{store_id}/check"). + // Template parameters in curly braces will be replaced using PathParameters. + Path string + + // PathParameters maps path template variables to their values. + // For example, for "/stores/{store_id}/check", provide {"store_id": "12345"}. + PathParameters map[string]string + + // QueryParameters contains URL query parameters. + QueryParameters url.Values + + // Body is the request payload (will be JSON encoded). + // Should typically be nil for GET/DELETE requests. + Body interface{} + + // Headers contains custom HTTP headers. + // Can override default headers like Content-Type and Accept if needed. + Headers map[string]string + + // TODO: Add support for request options like per-request timeouts, cancellation, retry options, etc. +} + +// APIExecutorRequestBuilder provides a fluent interface for building APIExecutorRequest instances. +type APIExecutorRequestBuilder struct { + request APIExecutorRequest +} + +// NewAPIExecutorRequestBuilder creates a new builder with required fields. +// operationName: descriptive name for the operation (e.g., "Check", "Write") +// method: HTTP method (GET, POST, PUT, DELETE, etc.) +// path: API path with optional template parameters (e.g., "/stores/{store_id}/check") +func NewAPIExecutorRequestBuilder(operationName, method, path string) *APIExecutorRequestBuilder { + return &APIExecutorRequestBuilder{ + request: APIExecutorRequest{ + OperationName: operationName, + Method: method, + Path: path, + PathParameters: make(map[string]string), + QueryParameters: url.Values{}, + Headers: make(map[string]string), + }, + } +} + +// WithPathParameter adds a single path parameter to the request. +// The parameter will be used to replace template variables in the path. +func (b *APIExecutorRequestBuilder) WithPathParameter(key, value string) *APIExecutorRequestBuilder { + if b.request.PathParameters == nil { + b.request.PathParameters = make(map[string]string) + } + + b.request.PathParameters[key] = value + return b +} + +// WithPathParameters sets all path parameters at once. +// Replaces any previously set path parameters. +func (b *APIExecutorRequestBuilder) WithPathParameters(params map[string]string) *APIExecutorRequestBuilder { + b.request.PathParameters = params + return b +} + +// WithQueryParameter adds a single query parameter to the request. +func (b *APIExecutorRequestBuilder) WithQueryParameter(key, value string) *APIExecutorRequestBuilder { + if b.request.QueryParameters == nil { + b.request.QueryParameters = url.Values{} + } + + b.request.QueryParameters.Add(key, value) + return b +} + +// WithQueryParameters sets all query parameters at once. +// Replaces any previously set query parameters. +func (b *APIExecutorRequestBuilder) WithQueryParameters(params url.Values) *APIExecutorRequestBuilder { + b.request.QueryParameters = params + return b +} + +// WithBody sets the request body (will be JSON encoded). +func (b *APIExecutorRequestBuilder) WithBody(body interface{}) *APIExecutorRequestBuilder { + b.request.Body = body + return b +} + +// WithHeader adds a single custom header to the request. +func (b *APIExecutorRequestBuilder) WithHeader(key, value string) *APIExecutorRequestBuilder { + if b.request.Headers == nil { + b.request.Headers = make(map[string]string) + } + + b.request.Headers[key] = value + return b +} + +// WithHeaders sets all custom headers at once. +// Replaces any previously set headers. +func (b *APIExecutorRequestBuilder) WithHeaders(headers map[string]string) *APIExecutorRequestBuilder { + b.request.Headers = headers + return b +} + +// Build returns the constructed APIExecutorRequest. +func (b *APIExecutorRequestBuilder) Build() APIExecutorRequest { + return b.request +} + +// APIExecutorResponse represents the response from an API execution. +type APIExecutorResponse struct { + // HTTPResponse is the raw HTTP response. + HTTPResponse *http.Response + + // Body contains the raw response body bytes. + Body []byte + + // StatusCode is the HTTP status code. + StatusCode int + + // Headers contains the response headers. + Headers http.Header +} + +// APIExecutor provides a generic interface for executing API requests with retry logic, telemetry, and error handling. +type APIExecutor interface { + // Execute performs an API request with automatic retry logic, telemetry, and error handling. + // It returns the raw response that can be decoded manually. + // + // Example using struct literal: + // openfga.APIExecutorRequest{ + // OperationName: "Check", + // Method: "POST", + // Path: "/stores/{store_id}/check", + // PathParameters: map[string]string{"store_id": storeID}, + // Body: checkRequest, + // } + // response, err := executor.Execute(ctx, request) + // + // Example using builder pattern: + // request := openfga.NewAPIExecutorRequestBuilder("Check", "POST", "/stores/{store_id}/check"). + // WithPathParameter("store_id", storeID). + // WithBody(checkRequest). + // Build() + // response, err := executor.Execute(ctx, request) + Execute(ctx context.Context, request APIExecutorRequest) (*APIExecutorResponse, error) + + // ExecuteWithDecode performs an API request and decodes the response into the provided result pointer. + // The result parameter must be a pointer to the type you want to decode into. + // + // Example using struct literal: + // var response openfga.CheckResponse + // openfga.APIExecutorRequest{ + // OperationName: "Check", + // Method: "POST", + // Path: "/stores/{store_id}/check", + // PathParameters: map[string]string{"store_id": storeID}, + // Body: checkRequest, + // } + // _, err := executor.ExecuteWithDecode(ctx, request, &response) + // + // Example using builder pattern: + // var response openfga.CheckResponse + // request := openfga.NewAPIExecutorRequestBuilder("Check", "POST", "/stores/{store_id}/check"). + // WithPathParameter("store_id", storeID). + // WithBody(checkRequest). + // Build() + // _, err := executor.ExecuteWithDecode(ctx, request, &response) + ExecuteWithDecode(ctx context.Context, request APIExecutorRequest, result interface{}) (*APIExecutorResponse, error) +} + +// validateRequest checks that required fields are present in the request. +func validateRequest(request APIExecutorRequest) error { + if request.OperationName == "" { + return reportError("operationName is required") + } + if request.Method == "" { + return reportError("method is required") + } + if request.Path == "" { + return reportError("path is required") + } + return nil +} + +// buildPath replaces template parameters in the path (e.g., {store_id}) with actual values. +func buildPath(template string, params map[string]string) string { + path := template + for key, value := range params { + placeholder := "{" + key + "}" + path = strings.ReplaceAll(path, placeholder, url.PathEscape(value)) + } + return path +} + +// prepareHeaders creates the header map with defaults and applies custom headers. +func prepareHeaders(customHeaders map[string]string) map[string]string { + headers := make(map[string]string) + headers["Content-Type"] = "application/json" + headers["Accept"] = "application/json" + + // Apply custom headers (may override defaults) + for key, value := range customHeaders { + headers[key] = value + } + + return headers +} + +// makeAPIExecutorResponse creates an APIExecutorResponse from an HTTP response and body. +func makeAPIExecutorResponse(httpResponse *http.Response, body []byte) *APIExecutorResponse { + return &APIExecutorResponse{ + HTTPResponse: httpResponse, + Body: body, + StatusCode: httpResponse.StatusCode, + Headers: httpResponse.Header, + } +} + +// apiExecutor is the internal implementation of APIExecutor. +type apiExecutor struct { + client *APIClient +} + +// NewAPIExecutor creates a new APIExecutor instance. +// This allows users to call any OpenFGA API endpoint, including those not yet supported by the SDK. +func NewAPIExecutor(client *APIClient) APIExecutor { + return &apiExecutor{client: client} +} + +// Execute performs an API request with automatic retry logic and error handling. +func (e *apiExecutor) Execute(ctx context.Context, request APIExecutorRequest) (*APIExecutorResponse, error) { + return e.executeInternal(ctx, request, nil) +} + +// ExecuteWithDecode performs an API request and decodes the response into the provided result pointer. +func (e *apiExecutor) ExecuteWithDecode(ctx context.Context, request APIExecutorRequest, result interface{}) (*APIExecutorResponse, error) { + return e.executeInternal(ctx, request, result) +} + +// executeInternal is the core execution logic used by both Execute and ExecuteWithDecode. +func (e *apiExecutor) executeInternal(ctx context.Context, request APIExecutorRequest, result interface{}) (*APIExecutorResponse, error) { + requestStarted := time.Now() + + // Validate required fields + if err := validateRequest(request); err != nil { + return nil, err + } + + // Build request parameters + path := buildPath(request.Path, request.PathParameters) + headerParams := prepareHeaders(request.Headers) + queryParams := request.QueryParameters + if queryParams == nil { + queryParams = url.Values{} + } + + // Get retry configuration + retryParams := e.getRetryParams() + storeID := request.PathParameters["store_id"] + + var lastResponse *APIExecutorResponse + + // Execute request with retry logic + for attemptNum := 0; attemptNum < retryParams.MaxRetry+1; attemptNum++ { + response, err := e.executeSingleAttempt(ctx, request, path, headerParams, queryParams, attemptNum, requestStarted, storeID) + if err == nil && response != nil { + // Decode response if needed + if result != nil { + if decodeErr := e.client.decode(result, response.Body, response.Headers.Get("Content-Type")); decodeErr != nil { + return response, GenericOpenAPIError{ + body: response.Body, + error: decodeErr.Error(), + } + } + } + return response, nil + } + + lastResponse = response + + // Check if we should retry + if attemptNum >= retryParams.MaxRetry { + return lastResponse, err + } + + // Determine if we should retry and how long to wait + if shouldRetry, waitDuration := e.determineRetry(err, response, attemptNum, retryParams, request.OperationName); shouldRetry { + if e.client.cfg.Debug { + e.logRetry(request, err, response, attemptNum, waitDuration) + } + time.Sleep(waitDuration) + continue + } + + // Error is not retryable + return lastResponse, err + } + + // All retries exhausted + if lastResponse != nil { + return lastResponse, reportError("max retries exceeded") + } + return nil, reportError("request failed without response") +} + +// getRetryParams returns the retry parameters, using defaults if not configured. +func (e *apiExecutor) getRetryParams() RetryParams { + if e.client.cfg.RetryParams != nil { + return *e.client.cfg.RetryParams + } + return RetryParams{ + MaxRetry: constants.DefaultMaxRetry, + MinWaitInMs: constants.DefaultMinWaitInMs, + } +} + +// recordTelemetry records request telemetry metrics. +func (e *apiExecutor) recordTelemetry(operationName string, storeID string, body interface{}, req *http.Request, httpResponse *http.Response, requestStarted time.Time, attemptNum int) { + metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: e.client.cfg.Telemetry}) + attrs, queryDuration, requestDuration, _ := metrics.BuildTelemetryAttributes( + operationName, + map[string]interface{}{ + "storeId": storeID, + "body": body, + }, + req, + httpResponse, + requestStarted, + attemptNum, + ) + + if requestDuration > 0 { + _, _ = metrics.RequestDuration(requestDuration, attrs) + } + + if queryDuration > 0 { + _, _ = metrics.QueryDuration(queryDuration, attrs) + } +} + +// executeSingleAttempt performs a single HTTP request attempt and handles the response. +func (e *apiExecutor) executeSingleAttempt( + ctx context.Context, + request APIExecutorRequest, + path string, + headerParams map[string]string, + queryParams url.Values, + attemptNum int, + requestStarted time.Time, + storeID string, +) (*APIExecutorResponse, error) { + // Prepare HTTP request + req, err := e.client.prepareRequest(ctx, path, request.Method, request.Body, headerParams, queryParams) + if err != nil { + return nil, err + } + + // Execute HTTP request + httpResponse, err := e.client.callAPI(req) + if err != nil || httpResponse == nil { + return nil, err + } + + // Read response body + responseBody, err := io.ReadAll(httpResponse.Body) + _ = httpResponse.Body.Close() + httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) + if err != nil { + return makeAPIExecutorResponse(httpResponse, responseBody), err + } + + response := makeAPIExecutorResponse(httpResponse, responseBody) + + // Handle HTTP errors (status >= 300) + if httpResponse.StatusCode >= http.StatusMultipleChoices { + apiErr := e.client.handleAPIError(httpResponse, responseBody, request.Body, request.OperationName, storeID) + return response, apiErr + } + + // Record telemetry for successful requests + e.recordTelemetry(request.OperationName, storeID, request.Body, req, httpResponse, requestStarted, attemptNum) + + return response, nil +} + +// determineRetry decides whether to retry a failed request and returns the wait duration. +func (e *apiExecutor) determineRetry( + err error, + response *APIExecutorResponse, + attemptNum int, + retryParams RetryParams, + operationName string, +) (bool, time.Duration) { + if err == nil { + return false, 0 + } + + // Check for rate limit or internal server errors that support retry + var rateLimitErr FgaApiRateLimitExceededError + var internalErr FgaApiInternalError + + switch { + case errors.As(err, &rateLimitErr): + timeToWait := rateLimitErr.GetTimeToWait(attemptNum, retryParams) + return timeToWait > 0, timeToWait + case errors.As(err, &internalErr): + timeToWait := internalErr.GetTimeToWait(attemptNum, retryParams) + return timeToWait > 0, timeToWait + default: + // Network errors or body read errors + headers := http.Header{} + if response != nil { + headers = response.Headers + } + timeToWait := retryutils.GetTimeToWait(attemptNum, retryParams.MaxRetry, retryParams.MinWaitInMs, headers, operationName) + return timeToWait > 0, timeToWait + } +} + +// logRetry logs retry information for debugging. +func (e *apiExecutor) logRetry(request APIExecutorRequest, err error, response *APIExecutorResponse, attemptNum int, waitDuration time.Duration) { + if response != nil { + log.Printf("\nWaiting %v to retry %v (attempt %d, status=%d, error=%v). Request body: %v\n", + waitDuration, request.OperationName, attemptNum, response.StatusCode, err, request.Body) + } else { + log.Printf("\nWaiting %v to retry %v (attempt %d, error=%v). Request body: %v\n", + waitDuration, request.OperationName, attemptNum, err, request.Body) + } +} diff --git a/api_open_fga.go b/api_open_fga.go index d2f17f2..3821a75 100644 --- a/api_open_fga.go +++ b/api_open_fga.go @@ -13,18 +13,10 @@ package openfga import ( - "bytes" "context" - "errors" - "io" - "log" "net/http" "net/url" - "strings" "time" - - "github.com/openfga/go-sdk/internal/utils/retryutils" - "github.com/openfga/go-sdk/telemetry" ) // Linger please @@ -1019,154 +1011,28 @@ func (a *OpenFgaApiService) BatchCheck(ctx context.Context, storeId string) ApiB * @return BatchCheckResponse */ func (a *OpenFgaApiService) BatchCheckExecute(r ApiBatchCheckRequest) (BatchCheckResponse, *http.Response, error) { - const ( - operationName = "BatchCheck" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue BatchCheckResponse - ) - - path := "/stores/{store_id}/batch-check" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue BatchCheckResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body + request := NewAPIExecutorRequestBuilder("BatchCheck", http.MethodPost, "/stores/{store_id}/batch-check"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiCheckRequest struct { @@ -1350,154 +1216,28 @@ func (a *OpenFgaApiService) Check(ctx context.Context, storeId string) ApiCheckR * @return CheckResponse */ func (a *OpenFgaApiService) CheckExecute(r ApiCheckRequest) (CheckResponse, *http.Response, error) { - const ( - operationName = "Check" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue CheckResponse - ) - - path := "/stores/{store_id}/check" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue CheckResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } - - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } + request := NewAPIExecutorRequestBuilder("Check", http.MethodPost, "/stores/{store_id}/check"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiCreateStoreRequest struct { @@ -1539,148 +1279,24 @@ func (a *OpenFgaApiService) CreateStore(ctx context.Context) ApiCreateStoreReque * @return CreateStoreResponse */ func (a *OpenFgaApiService) CreateStoreExecute(r ApiCreateStoreRequest) (CreateStoreResponse, *http.Response, error) { - const ( - operationName = "CreateStore" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue CreateStoreResponse - ) - - path := "/stores" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + var returnValue CreateStoreResponse + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // body params - requestBody = r.body - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } + executor := a.client.GetAPIExecutor() - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } + request := NewAPIExecutorRequestBuilder("CreateStore", http.MethodPost, "/stores"). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, "") - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiDeleteStoreRequest struct { @@ -1718,139 +1334,23 @@ func (a *OpenFgaApiService) DeleteStore(ctx context.Context, storeId string) Api * Execute executes the request */ func (a *OpenFgaApiService) DeleteStoreExecute(r ApiDeleteStoreRequest) (*http.Response, error) { - const ( - operationName = "DeleteStore" - httpMethod = http.MethodDelete - ) - var ( - requestStarted = time.Now() - requestBody interface{} - ) - - path := "/stores/{store_id}" - if r.storeId == "" { - return nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + if err := validatePathParameter("storeId", r.storeId); err != nil { + return nil, err } - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } + request := NewAPIExecutorRequestBuilder("DeleteStore", http.MethodDelete, "/stores/{store_id}"). + WithPathParameter("store_id", r.storeId). + WithHeaders(r.options.Headers). + Build() + response, err := executor.Execute(r.ctx, request) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return httpResponse, err - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return httpResponse, nil - } - - // should never have reached this - return nil, reportError("Error not handled properly") + return nil, err } type ApiExpandRequest struct { @@ -2072,154 +1572,28 @@ func (a *OpenFgaApiService) Expand(ctx context.Context, storeId string) ApiExpan * @return ExpandResponse */ func (a *OpenFgaApiService) ExpandExecute(r ApiExpandRequest) (ExpandResponse, *http.Response, error) { - const ( - operationName = "Expand" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ExpandResponse - ) - - path := "/stores/{store_id}/expand" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + var returnValue ExpandResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } + executor := a.client.GetAPIExecutor() - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("Expand", http.MethodPost, "/stores/{store_id}/expand"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiGetStoreRequest struct { @@ -2258,149 +1632,24 @@ func (a *OpenFgaApiService) GetStore(ctx context.Context, storeId string) ApiGet * @return GetStoreResponse */ func (a *OpenFgaApiService) GetStoreExecute(r ApiGetStoreRequest) (GetStoreResponse, *http.Response, error) { - const ( - operationName = "GetStore" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue GetStoreResponse - ) - - path := "/stores/{store_id}" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue GetStoreResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } + executor := a.client.GetAPIExecutor() - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + request := NewAPIExecutorRequestBuilder("GetStore", http.MethodGet, "/stores/{store_id}"). + WithPathParameter("store_id", r.storeId). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + if response != nil { + return returnValue, response.HTTPResponse, err } - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } - - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiListObjectsRequest struct { @@ -2454,154 +1703,28 @@ func (a *OpenFgaApiService) ListObjects(ctx context.Context, storeId string) Api * @return ListObjectsResponse */ func (a *OpenFgaApiService) ListObjectsExecute(r ApiListObjectsRequest) (ListObjectsResponse, *http.Response, error) { - const ( - operationName = "ListObjects" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ListObjectsResponse - ) - - path := "/stores/{store_id}/list-objects" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + var returnValue ListObjectsResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } + executor := a.client.GetAPIExecutor() - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("ListObjects", http.MethodPost, "/stores/{store_id}/list-objects"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiListStoresRequest struct { @@ -2656,152 +1779,32 @@ func (a *OpenFgaApiService) ListStores(ctx context.Context) ApiListStoresRequest * @return ListStoresResponse */ func (a *OpenFgaApiService) ListStoresExecute(r ApiListStoresRequest) (ListStoresResponse, *http.Response, error) { - const ( - operationName = "ListStores" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ListStoresResponse - ) + var returnValue ListStoresResponse - path := "/stores" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} + executor := a.client.GetAPIExecutor() + queryParams := url.Values{} if r.pageSize != nil { - localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) + queryParams.Add("page_size", parameterToString(*r.pageSize, "")) } if r.continuationToken != nil { - localVarQueryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) + queryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) } if r.name != nil { - localVarQueryParams.Add("name", parameterToString(*r.name, "")) - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + queryParams.Add("name", parameterToString(*r.name, "")) } - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } - - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, "") - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("ListStores", http.MethodGet, "/stores"). + WithQueryParameters(queryParams). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiListUsersRequest struct { @@ -2856,154 +1859,28 @@ func (a *OpenFgaApiService) ListUsers(ctx context.Context, storeId string) ApiLi * @return ListUsersResponse */ func (a *OpenFgaApiService) ListUsersExecute(r ApiListUsersRequest) (ListUsersResponse, *http.Response, error) { - const ( - operationName = "ListUsers" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ListUsersResponse - ) - - path := "/stores/{store_id}/list-users" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue ListUsersResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } + executor := a.client.GetAPIExecutor() - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("ListUsers", http.MethodPost, "/stores/{store_id}/list-users"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiReadRequest struct { @@ -3159,154 +2036,28 @@ func (a *OpenFgaApiService) Read(ctx context.Context, storeId string) ApiReadReq * @return ReadResponse */ func (a *OpenFgaApiService) ReadExecute(r ApiReadRequest) (ReadResponse, *http.Response, error) { - const ( - operationName = "Read" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ReadResponse - ) - - path := "/stores/{store_id}/read" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue ReadResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body + request := NewAPIExecutorRequestBuilder("Read", http.MethodPost, "/stores/{store_id}/read"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiReadAssertionsRequest struct { @@ -3348,154 +2099,28 @@ func (a *OpenFgaApiService) ReadAssertions(ctx context.Context, storeId string, * @return ReadAssertionsResponse */ func (a *OpenFgaApiService) ReadAssertionsExecute(r ApiReadAssertionsRequest) (ReadAssertionsResponse, *http.Response, error) { - const ( - operationName = "ReadAssertions" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ReadAssertionsResponse - ) - - path := "/stores/{store_id}/assertions/{authorization_model_id}" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue ReadAssertionsResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - if r.authorizationModelId == "" { - return returnValue, nil, reportError("authorizationModelId is required and must be specified") + if err := validatePathParameter("authorizationModelId", r.authorizationModelId); err != nil { + return returnValue, nil, err } - path = strings.ReplaceAll(path, "{"+"authorization_model_id"+"}", url.PathEscape(parameterToString(r.authorizationModelId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } + executor := a.client.GetAPIExecutor() - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + request := NewAPIExecutorRequestBuilder("ReadAssertions", http.MethodGet, "/stores/{store_id}/assertions/{authorization_model_id}"). + WithPathParameter("store_id", r.storeId). + WithPathParameter("authorization_model_id", r.authorizationModelId). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + if response != nil { + return returnValue, response.HTTPResponse, err } - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } - - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiReadAuthorizationModelRequest struct { @@ -3580,154 +2205,28 @@ func (a *OpenFgaApiService) ReadAuthorizationModel(ctx context.Context, storeId * @return ReadAuthorizationModelResponse */ func (a *OpenFgaApiService) ReadAuthorizationModelExecute(r ApiReadAuthorizationModelRequest) (ReadAuthorizationModelResponse, *http.Response, error) { - const ( - operationName = "ReadAuthorizationModel" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ReadAuthorizationModelResponse - ) - - path := "/stores/{store_id}/authorization-models/{id}" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - if r.id == "" { - return returnValue, nil, reportError("id is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"id"+"}", url.PathEscape(parameterToString(r.id, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + var returnValue ReadAuthorizationModelResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + if err := validatePathParameter("id", r.id); err != nil { + return returnValue, nil, err } - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val - } + executor := a.client.GetAPIExecutor() - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } + request := NewAPIExecutorRequestBuilder("ReadAuthorizationModel", http.MethodGet, "/stores/{store_id}/authorization-models/{id}"). + WithPathParameter("store_id", r.storeId). + WithPathParameter("id", r.id). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiReadAuthorizationModelsRequest struct { @@ -3818,155 +2317,33 @@ func (a *OpenFgaApiService) ReadAuthorizationModels(ctx context.Context, storeId * @return ReadAuthorizationModelsResponse */ func (a *OpenFgaApiService) ReadAuthorizationModelsExecute(r ApiReadAuthorizationModelsRequest) (ReadAuthorizationModelsResponse, *http.Response, error) { - const ( - operationName = "ReadAuthorizationModels" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ReadAuthorizationModelsResponse - ) - - path := "/stores/{store_id}/authorization-models" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue ReadAuthorizationModelsResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} + executor := a.client.GetAPIExecutor() + queryParams := url.Values{} if r.pageSize != nil { - localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) + queryParams.Add("page_size", parameterToString(*r.pageSize, "")) } if r.continuationToken != nil { - localVarQueryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + queryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) } - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } + request := NewAPIExecutorRequestBuilder("ReadAuthorizationModels", http.MethodGet, "/stores/{store_id}/authorization-models"). + WithPathParameter("store_id", r.storeId). + WithQueryParameters(queryParams). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiReadChangesRequest struct { @@ -4031,161 +2408,39 @@ func (a *OpenFgaApiService) ReadChanges(ctx context.Context, storeId string) Api * @return ReadChangesResponse */ func (a *OpenFgaApiService) ReadChangesExecute(r ApiReadChangesRequest) (ReadChangesResponse, *http.Response, error) { - const ( - operationName = "ReadChanges" - httpMethod = http.MethodGet - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue ReadChangesResponse - ) - - path := "/stores/{store_id}/changes" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue ReadChangesResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} + executor := a.client.GetAPIExecutor() + queryParams := url.Values{} if r.type_ != nil { - localVarQueryParams.Add("type", parameterToString(*r.type_, "")) + queryParams.Add("type", parameterToString(*r.type_, "")) } if r.pageSize != nil { - localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) + queryParams.Add("page_size", parameterToString(*r.pageSize, "")) } if r.continuationToken != nil { - localVarQueryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) + queryParams.Add("continuation_token", parameterToString(*r.continuationToken, "")) } if r.startTime != nil { - localVarQueryParams.Add("start_time", parameterToString(*r.startTime, "")) - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + queryParams.Add("start_time", parameterToString(*r.startTime, "")) } - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } + request := NewAPIExecutorRequestBuilder("ReadChanges", http.MethodGet, "/stores/{store_id}/changes"). + WithPathParameter("store_id", r.storeId). + WithQueryParameters(queryParams). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiStreamedListObjectsRequest struct { @@ -4234,154 +2489,28 @@ func (a *OpenFgaApiService) StreamedListObjects(ctx context.Context, storeId str * @return StreamResultOfStreamedListObjectsResponse */ func (a *OpenFgaApiService) StreamedListObjectsExecute(r ApiStreamedListObjectsRequest) (StreamResultOfStreamedListObjectsResponse, *http.Response, error) { - const ( - operationName = "StreamedListObjects" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue StreamResultOfStreamedListObjectsResponse - ) - - path := "/stores/{store_id}/streamed-list-objects" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") + var returnValue StreamResultOfStreamedListObjectsResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } + request := NewAPIExecutorRequestBuilder("StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiWriteRequest struct { @@ -4473,154 +2602,28 @@ func (a *OpenFgaApiService) Write(ctx context.Context, storeId string) ApiWriteR * @return map[string]interface{} */ func (a *OpenFgaApiService) WriteExecute(r ApiWriteRequest) (map[string]interface{}, *http.Response, error) { - const ( - operationName = "Write" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue map[string]interface{} - ) - - path := "/stores/{store_id}/write" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") + var returnValue map[string]interface{} + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} + executor := a.client.GetAPIExecutor() - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - // body params - requestBody = r.body + request := NewAPIExecutorRequestBuilder("Write", http.MethodPost, "/stores/{store_id}/write"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if response != nil { + return returnValue, response.HTTPResponse, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } - - return returnValue, httpResponse, nil - } - - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } type ApiWriteAssertionsRequest struct { @@ -4667,149 +2670,31 @@ func (a *OpenFgaApiService) WriteAssertions(ctx context.Context, storeId string, * Execute executes the request */ func (a *OpenFgaApiService) WriteAssertionsExecute(r ApiWriteAssertionsRequest) (*http.Response, error) { - const ( - operationName = "WriteAssertions" - httpMethod = http.MethodPut - ) - var ( - requestStarted = time.Now() - requestBody interface{} - ) - - path := "/stores/{store_id}/assertions/{authorization_model_id}" - if r.storeId == "" { - return nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - if r.authorizationModelId == "" { - return nil, reportError("authorizationModelId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"authorization_model_id"+"}", url.PathEscape(parameterToString(r.authorizationModelId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType + if err := validatePathParameter("storeId", r.storeId); err != nil { + return nil, err } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + if err := validatePathParameter("authorizationModelId", r.authorizationModelId); err != nil { + return nil, err } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if err := validateParameter("body", r.body); err != nil { + return nil, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return httpResponse, err - } - - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } + executor := a.client.GetAPIExecutor() - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("WriteAssertions", http.MethodPut, "/stores/{store_id}/assertions/{authorization_model_id}"). + WithPathParameter("store_id", r.storeId). + WithPathParameter("authorization_model_id", r.authorizationModelId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.Execute(r.ctx, request) - return httpResponse, nil + if response != nil { + return response.HTTPResponse, err } - // should never have reached this - return nil, reportError("Error not handled properly") + return nil, err } type ApiWriteAuthorizationModelRequest struct { @@ -4899,152 +2784,26 @@ func (a *OpenFgaApiService) WriteAuthorizationModel(ctx context.Context, storeId * @return WriteAuthorizationModelResponse */ func (a *OpenFgaApiService) WriteAuthorizationModelExecute(r ApiWriteAuthorizationModelRequest) (WriteAuthorizationModelResponse, *http.Response, error) { - const ( - operationName = "WriteAuthorizationModel" - httpMethod = http.MethodPost - ) - var ( - requestStarted = time.Now() - requestBody interface{} - returnValue WriteAuthorizationModelResponse - ) - - path := "/stores/{store_id}/authorization-models" - if r.storeId == "" { - return returnValue, nil, reportError("storeId is required and must be specified") - } - - path = strings.ReplaceAll(path, "{"+"store_id"+"}", url.PathEscape(parameterToString(r.storeId, ""))) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - if r.body == nil { - return returnValue, nil, reportError("body is required and must be specified") - } - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{"application/json"} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + var returnValue WriteAuthorizationModelResponse + if err := validatePathParameter("storeId", r.storeId); err != nil { + return returnValue, nil, err } - // body params - requestBody = r.body - - // if any override headers were in the options, set them now - for header, val := range r.options.Headers { - localVarHeaderParams[header] = val + if err := validateParameter("body", r.body); err != nil { + return returnValue, nil, err } - retryParams := a.client.cfg.RetryParams - for i := 0; i < retryParams.MaxRetry+1; i++ { - req, err := a.client.prepareRequest(r.ctx, path, httpMethod, requestBody, localVarHeaderParams, localVarQueryParams) - if err != nil { - return returnValue, nil, err - } - - httpResponse, err := a.client.callAPI(req) - if err != nil || httpResponse == nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, http.Header{}, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to network error (error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - responseBody, err := io.ReadAll(httpResponse.Body) - _ = httpResponse.Body.Close() - httpResponse.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - if err != nil { - if i < retryParams.MaxRetry { - timeToWait := retryutils.GetTimeToWait(i, retryParams.MaxRetry, retryParams.MinWaitInMs, httpResponse.Header, operationName) - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to error parsing response body (err=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - return returnValue, httpResponse, err - } - - if httpResponse.StatusCode >= http.StatusMultipleChoices { - err := a.client.handleAPIError(httpResponse, responseBody, requestBody, operationName, r.storeId) - if err != nil && i < retryParams.MaxRetry { - timeToWait := time.Duration(0) - var fgaApiRateLimitExceededError FgaApiRateLimitExceededError - var fgaApiInternalError FgaApiInternalError - switch { - case errors.As(err, &fgaApiRateLimitExceededError): - timeToWait = err.(FgaApiRateLimitExceededError).GetTimeToWait(i, *retryParams) - case errors.As(err, &fgaApiInternalError): - timeToWait = err.(FgaApiInternalError).GetTimeToWait(i, *retryParams) - } - - if timeToWait > 0 { - if a.client.cfg.Debug { - log.Printf("\nWaiting %v to retry %v (%v %v) due to api retryable error (status code %v, error=%v) on attempt %v. Request body: %v\n", timeToWait, operationName, req.Method, req.URL, httpResponse.StatusCode, err, i, requestBody) - } - time.Sleep(timeToWait) - continue - } - } - - return returnValue, httpResponse, err - } - - err = a.client.decode(&returnValue, responseBody, httpResponse.Header.Get("Content-Type")) - if err != nil { - newErr := GenericOpenAPIError{ - body: responseBody, - error: err.Error(), - } - return returnValue, httpResponse, newErr - } + executor := a.client.GetAPIExecutor() - metrics := telemetry.GetMetrics(telemetry.TelemetryFactoryParameters{Configuration: a.client.cfg.Telemetry}) - - var attrs, queryDuration, requestDuration, _ = metrics.BuildTelemetryAttributes( - operationName, - map[string]interface{}{ - "storeId": r.storeId, - "body": requestBody, - }, - req, - httpResponse, - requestStarted, - i, - ) - - if requestDuration > 0 { - _, _ = metrics.RequestDuration(requestDuration, attrs) - } - - if queryDuration > 0 { - _, _ = metrics.QueryDuration(queryDuration, attrs) - } + request := NewAPIExecutorRequestBuilder("WriteAuthorizationModel", http.MethodPost, "/stores/{store_id}/authorization-models"). + WithPathParameter("store_id", r.storeId). + WithBody(r.body). + WithHeaders(r.options.Headers). + Build() + response, err := executor.ExecuteWithDecode(r.ctx, request, &returnValue) - return returnValue, httpResponse, nil + if response != nil { + return returnValue, response.HTTPResponse, err } - // should never have reached this - return returnValue, nil, reportError("Error not handled properly") + return returnValue, nil, err } diff --git a/example/example1/example1.go b/example/example1/example1.go index d7853c7..e4f1981 100644 --- a/example/example1/example1.go +++ b/example/example1/example1.go @@ -2,7 +2,9 @@ package main import ( "context" + "encoding/json" "fmt" + "net/http" "os" openfga "github.com/openfga/go-sdk" @@ -58,7 +60,8 @@ func mainInner() error { fmt.Printf("Test Store ID: %v\n", store.Id) // Set the store id - fgaClient.SetStoreId(store.Id) + storeID := store.Id + fgaClient.SetStoreId(storeID) // ListStores after Create fmt.Println("Listing Stores") @@ -240,31 +243,27 @@ func mainInner() error { Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", }).Execute() if err != nil { - fmt.Printf("Failed due to: %w\n", err.Error()) + fmt.Printf("Failed due to: %s\n", err.Error()) } else { fmt.Printf("Allowed: %v\n", failingCheckResponse.Allowed) } // Checking for access with context fmt.Println("Checking for access with context") - checkResponse, err := fgaClient.Check(ctx).Body(client.ClientCheckRequest{ + checkRequestWithContext := client.ClientCheckRequest{ User: "user:anne", Relation: "viewer", Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", Context: &map[string]interface{}{"ViewCount": 100}, - }).Execute() + } + checkResponse, err := fgaClient.Check(ctx).Body(checkRequestWithContext).Execute() if err != nil { return err } fmt.Printf("Allowed: %v\n", checkResponse.Allowed) fmt.Println("Checking for access with custom headers") - checkWithHeadersResponse, err := fgaClient.Check(ctx).Body(client.ClientCheckRequest{ - User: "user:anne", - Relation: "viewer", - Object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", - Context: &map[string]interface{}{"ViewCount": 100}, - }).Options(client.ClientCheckOptions{ + checkWithHeadersResponse, err := fgaClient.Check(ctx).Body(checkRequestWithContext).Options(client.ClientCheckOptions{ RequestOptions: client.RequestOptions{ Headers: map[string]string{ "X-Request-ID": "example-request-123", @@ -276,6 +275,39 @@ func mainInner() error { } fmt.Printf("Allowed (with custom headers): %v\n", checkWithHeadersResponse.Allowed) + // Checking for access using a custom executor + fmt.Println("Checking for access with context") + + // Get the API executor + executor := fgaClient.GetAPIExecutor() + + customRequest := openfga.NewAPIExecutorRequestBuilder("Check", http.MethodPost, "/stores/{store_id}/check"). + WithPathParameter("store_id", storeID). + WithBody(checkRequestWithContext). + WithHeader("X-Experimental-Feature", "enabled"). + Build() + + // custom executor + decoded response + var checkResponseCustomExecutorWithDecode openfga.CheckResponse + _, err = executor.ExecuteWithDecode(ctx, customRequest, &checkResponseCustomExecutorWithDecode) + if err != nil { + return err + } + fmt.Printf("Allowed (with custom executor + decode): %v\n", checkResponseCustomExecutorWithDecode.Allowed) + + // custom executor + raw response + checkResponseCustomExecutorWithRawResponse, err := executor.Execute(ctx, customRequest) + if err != nil { + return err + } + + var checkRawResponse openfga.CheckResponse + if err := json.Unmarshal(checkResponseCustomExecutorWithRawResponse.Body, &checkRawResponse); err != nil { + fmt.Printf("Failed to decode response: %v", err) + } else { + fmt.Printf("Allowed (with custom executor with raw response): %v\n", checkRawResponse.Allowed) + } + // BatchCheck fmt.Println("Batch checking for access") batchCheckResponse, err := fgaClient.BatchCheck(ctx).Body(client.ClientBatchCheckRequest{ diff --git a/utils.go b/utils.go index 260d50c..06bb218 100644 --- a/utils.go +++ b/utils.go @@ -340,3 +340,17 @@ func IsWellFormedUri(uriString string) bool { return true } + +func validatePathParameter(name string, value string) error { + if value == "" { + return reportError("%s is required and must be specified", name) + } + return nil +} + +func validateParameter(name string, value interface{}) error { + if value == nil { + return reportError("%s is required and must be specified", name) + } + return nil +} From 43823138408253b788813974c38765939cb6fa26 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Wed, 19 Nov 2025 19:20:10 -0500 Subject: [PATCH 2/7] chore: add tests for the custom api executor --- api_executor_test.go | 1252 ++++++++++++++++++++++++++++++++++++++++++ utils_test.go | 150 +++++ 2 files changed, 1402 insertions(+) create mode 100644 api_executor_test.go diff --git a/api_executor_test.go b/api_executor_test.go new file mode 100644 index 0000000..42a6c1c --- /dev/null +++ b/api_executor_test.go @@ -0,0 +1,1252 @@ +package openfga + +import ( + "context" + "errors" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openfga/go-sdk/internal/constants" +) + +// Test helpers + +// testRoundTripper to allow stubbing HTTP responses. +type testRoundTripper struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return t.fn(req) } + +// helper to build a http.Response quickly. +func makeResp(status int, body string, headers map[string]string) *http.Response { + h := http.Header{} + for k, v := range headers { + h.Set(k, v) + } + return &http.Response{ + StatusCode: status, + Body: io.NopCloser(strings.NewReader(body)), + Header: h, + } +} + +// build a minimal APIClient wired with custom http.Client +func newTestClient(t *testing.T, rt http.RoundTripper, retry *RetryParams) *APIClient { + t.Helper() + if retry == nil { + retry = &RetryParams{MaxRetry: 0, MinWaitInMs: 1} + } + cfg := &Configuration{ApiUrl: constants.TestApiUrl, RetryParams: retry, Debug: false, HTTPClient: &http.Client{Transport: rt}} + return NewAPIClient(cfg) +} + +// Tests + +func TestValidateRequest(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + request APIExecutorRequest + expectError bool + errorMsg string + }{ + { + name: "valid_request_all_fields", + request: APIExecutorRequest{ + OperationName: "Check", + Method: "POST", + Path: "/stores/{store_id}/check", + }, + expectError: false, + }, + { + name: "valid_request_minimal", + request: APIExecutorRequest{ + OperationName: "Read", + Method: "GET", + Path: "/read", + }, + expectError: false, + }, + { + name: "missing_operation_name", + request: APIExecutorRequest{ + Method: "POST", + Path: "/check", + }, + expectError: true, + errorMsg: "operationName is required", + }, + { + name: "missing_method", + request: APIExecutorRequest{ + OperationName: "Check", + Path: "/check", + }, + expectError: true, + errorMsg: "method is required", + }, + { + name: "missing_path", + request: APIExecutorRequest{ + OperationName: "Check", + Method: "POST", + }, + expectError: true, + errorMsg: "path is required", + }, + { + name: "empty_operation_name", + request: APIExecutorRequest{ + OperationName: "", + Method: "POST", + Path: "/check", + }, + expectError: true, + errorMsg: "operationName is required", + }, + { + name: "empty_method", + request: APIExecutorRequest{ + OperationName: "Check", + Method: "", + Path: "/check", + }, + expectError: true, + errorMsg: "method is required", + }, + { + name: "empty_path", + request: APIExecutorRequest{ + OperationName: "Check", + Method: "POST", + Path: "", + }, + expectError: true, + errorMsg: "path is required", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := validateRequest(tc.request) + + if tc.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestBuildPath(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + template string + params map[string]string + expectedPath string + }{ + { + name: "no_parameters", + template: "/stores", + params: map[string]string{}, + expectedPath: "/stores", + }, + { + name: "single_parameter", + template: "/stores/{store_id}", + params: map[string]string{ + "store_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + }, + expectedPath: "/stores/01ARZ3NDEKTSV4RRFFQ69G5FAV", + }, + { + name: "multiple_parameters", + template: "/stores/{store_id}/models/{model_id}", + params: map[string]string{ + "store_id": "store-123", + "model_id": "model-456", + }, + expectedPath: "/stores/store-123/models/model-456", + }, + { + name: "parameter_with_special_characters", + template: "/stores/{store_id}", + params: map[string]string{ + "store_id": "store id with spaces", + }, + expectedPath: "/stores/store%20id%20with%20spaces", + }, + { + name: "parameter_with_url_unsafe_characters", + template: "/items/{id}", + params: map[string]string{ + "id": "test/with?special&chars", + }, + expectedPath: "/items/test%2Fwith%3Fspecial&chars", + }, + { + name: "unused_parameters_ignored", + template: "/stores/{store_id}", + params: map[string]string{ + "store_id": "123", + "unused": "value", + }, + expectedPath: "/stores/123", + }, + { + name: "parameter_appears_multiple_times", + template: "/stores/{id}/check/{id}", + params: map[string]string{ + "id": "abc", + }, + expectedPath: "/stores/abc/check/abc", + }, + { + name: "nil_params", + template: "/stores/{store_id}", + params: nil, + expectedPath: "/stores/{store_id}", + }, + { + name: "empty_parameter_value", + template: "/stores/{store_id}", + params: map[string]string{ + "store_id": "", + }, + expectedPath: "/stores/", + }, + { + name: "parameter_with_unicode", + template: "/users/{name}", + params: map[string]string{ + "name": "η”¨ζˆ·", + }, + expectedPath: "/users/%E7%94%A8%E6%88%B7", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := buildPath(tc.template, tc.params) + assert.Equal(t, tc.expectedPath, result) + }) + } +} + +func TestPrepareHeaders(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + customHeaders map[string]string + expectedHeaders map[string]string + }{ + { + name: "no_custom_headers", + customHeaders: map[string]string{}, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + }, + }, + { + name: "nil_custom_headers", + customHeaders: nil, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + }, + }, + { + name: "custom_header_added", + customHeaders: map[string]string{ + "X-Custom-Header": "custom-value", + }, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Custom-Header": "custom-value", + }, + }, + { + name: "override_content_type", + customHeaders: map[string]string{ + "Content-Type": "application/xml", + }, + expectedHeaders: map[string]string{ + "Content-Type": "application/xml", + "Accept": "application/json", + }, + }, + { + name: "override_accept", + customHeaders: map[string]string{ + "Accept": "application/vnd.api+json", + }, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/vnd.api+json", + }, + }, + { + name: "override_both_defaults", + customHeaders: map[string]string{ + "Content-Type": "text/plain", + "Accept": "text/html", + }, + expectedHeaders: map[string]string{ + "Content-Type": "text/plain", + "Accept": "text/html", + }, + }, + { + name: "multiple_custom_headers", + customHeaders: map[string]string{ + "Authorization": "Bearer token123", + "X-Request-ID": "req-456", + "X-API-Key": "key789", + }, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer token123", + "X-Request-ID": "req-456", + "X-API-Key": "key789", + }, + }, + { + name: "case_sensitive_headers", + customHeaders: map[string]string{ + "content-type": "should-override", + }, + expectedHeaders: map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "content-type": "should-override", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := prepareHeaders(tc.customHeaders) + assert.Equal(t, tc.expectedHeaders, result) + }) + } +} + +func TestMakeAPIExecutorResponse(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + httpResponse *http.Response + body []byte + }{ + { + name: "success_response", + httpResponse: &http.Response{ + StatusCode: 200, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "X-Request-ID": []string{"req-123"}, + }, + }, + body: []byte(`{"message":"success"}`), + }, + { + name: "error_response", + httpResponse: &http.Response{ + StatusCode: 404, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + }, + body: []byte(`{"code":"not_found","message":"Resource not found"}`), + }, + { + name: "empty_body", + httpResponse: &http.Response{ + StatusCode: 204, + Header: http.Header{}, + }, + body: []byte{}, + }, + { + name: "large_body", + httpResponse: &http.Response{ + StatusCode: 200, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + }, + body: []byte(strings.Repeat("x", 10000)), + }, + { + name: "multiple_header_values", + httpResponse: &http.Response{ + StatusCode: 200, + Header: http.Header{ + "Set-Cookie": []string{"session=abc123", "preferences=dark_mode"}, + }, + }, + body: []byte(`{"ok":true}`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := makeAPIExecutorResponse(tc.httpResponse, tc.body) + + require.NotNil(t, result) + assert.Equal(t, tc.httpResponse, result.HTTPResponse) + assert.Equal(t, tc.body, result.Body) + assert.Equal(t, tc.httpResponse.StatusCode, result.StatusCode) + assert.Equal(t, tc.httpResponse.Header, result.Headers) + }) + } +} + +func TestAPIExecutorRequestBuilder_NilMaps(t *testing.T) { + t.Parallel() + + t.Run("with_path_parameter_initializes_nil_map", func(t *testing.T) { + t.Parallel() + + builder := &APIExecutorRequestBuilder{ + request: APIExecutorRequest{ + PathParameters: nil, + }, + } + + result := builder.WithPathParameter("key", "value") + + assert.NotNil(t, result.request.PathParameters) + assert.Equal(t, "value", result.request.PathParameters["key"]) + }) + + t.Run("with_query_parameter_initializes_nil_map", func(t *testing.T) { + t.Parallel() + + builder := &APIExecutorRequestBuilder{ + request: APIExecutorRequest{ + QueryParameters: nil, + }, + } + + result := builder.WithQueryParameter("key", "value") + + assert.NotNil(t, result.request.QueryParameters) + assert.Equal(t, "value", result.request.QueryParameters.Get("key")) + }) + + t.Run("with_header_initializes_nil_map", func(t *testing.T) { + t.Parallel() + + builder := &APIExecutorRequestBuilder{ + request: APIExecutorRequest{ + Headers: nil, + }, + } + + result := builder.WithHeader("key", "value") + + assert.NotNil(t, result.request.Headers) + assert.Equal(t, "value", result.request.Headers["key"]) + }) +} + +func TestAPIExecutorRequestBuilder_MultipleQueryValues(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithQueryParameter("tag", "go"). + WithQueryParameter("tag", "golang"). + WithQueryParameter("tag", "api") + + req := builder.Build() + + tags := req.QueryParameters["tag"] + assert.Len(t, tags, 3) + assert.Contains(t, tags, "go") + assert.Contains(t, tags, "golang") + assert.Contains(t, tags, "api") +} + +func TestAPIExecutorRequestBuilder_PathParameterOverwrite(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/stores/{store_id}") + + builder.WithPathParameter("store_id", "old-value"). + WithPathParameter("store_id", "new-value") + + req := builder.Build() + + assert.Equal(t, "new-value", req.PathParameters["store_id"]) +} + +func TestAPIExecutorRequestBuilder_QueryParameterReplace(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithQueryParameter("page", "1") + + newParams := url.Values{} + newParams.Add("page", "2") + newParams.Add("limit", "10") + + builder.WithQueryParameters(newParams) + + req := builder.Build() + + assert.Equal(t, "2", req.QueryParameters.Get("page")) + assert.Equal(t, "10", req.QueryParameters.Get("limit")) +} + +func TestAPIExecutorRequestBuilder_HeaderReplace(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithHeader("X-Old", "old-value") + + newHeaders := map[string]string{ + "X-New": "new-value", + "X-API": "api-key", + } + + builder.WithHeaders(newHeaders) + + req := builder.Build() + + assert.Equal(t, "new-value", req.Headers["X-New"]) + assert.Equal(t, "api-key", req.Headers["X-API"]) + _, exists := req.Headers["X-Old"] + assert.False(t, exists, "Old header should be replaced") +} + +func TestAPIExecutorRequestBuilder_BodyTypes(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + body interface{} + }{ + { + name: "string_body", + body: "test string", + }, + { + name: "struct_body", + body: struct { + Name string + Value int + }{Name: "test", Value: 123}, + }, + { + name: "map_body", + body: map[string]interface{}{"key": "value", "number": 42}, + }, + { + name: "slice_body", + body: []string{"a", "b", "c"}, + }, + { + name: "nil_body", + body: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "POST", "/test") + builder.WithBody(tc.body) + req := builder.Build() + + assert.Equal(t, tc.body, req.Body) + }) + } +} + +func TestNewAPIExecutor(t *testing.T) { + t.Parallel() + + t.Run("creates_executor_with_valid_client", func(t *testing.T) { + t.Parallel() + + client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { + return makeResp(200, `{"ok":true}`, nil), nil + }}, nil) + + executor := NewAPIExecutor(client) + + assert.NotNil(t, executor) + }) + + t.Run("executor_can_execute_request", func(t *testing.T) { + t.Parallel() + + client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { + return makeResp(200, `{"message":"ok"}`, map[string]string{"Content-Type": "application/json"}), nil + }}, nil) + + executor := NewAPIExecutor(client) + resp, err := executor.Execute(context.Background(), APIExecutorRequest{ + OperationName: "Test", + Method: "GET", + Path: "/test", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, 200, resp.StatusCode) + }) +} + +func TestAPIExecutor_GetRetryParams(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + configRetryParams *RetryParams + expectedMaxRetry int + expectedMinWaitInMs int + }{ + { + name: "uses_configured_retry_params", + configRetryParams: &RetryParams{ + MaxRetry: 5, + MinWaitInMs: 100, + }, + expectedMaxRetry: 5, + expectedMinWaitInMs: 100, + }, + { + name: "uses_custom_values", + configRetryParams: &RetryParams{ + MaxRetry: 10, + MinWaitInMs: 500, + }, + expectedMaxRetry: 10, + expectedMinWaitInMs: 500, + }, + { + name: "uses_different_retry_values", + configRetryParams: &RetryParams{ + MaxRetry: 2, + MinWaitInMs: 200, + }, + expectedMaxRetry: 2, + expectedMinWaitInMs: 200, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { + return makeResp(200, `{"ok":true}`, nil), nil + }}, tc.configRetryParams) + + executor := NewAPIExecutor(client).(*apiExecutor) + retryParams := executor.getRetryParams() + + assert.Equal(t, tc.expectedMaxRetry, retryParams.MaxRetry) + assert.Equal(t, tc.expectedMinWaitInMs, retryParams.MinWaitInMs) + }) + } +} + +func TestAPIExecutor_DetermineRetry(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + err error + response *APIExecutorResponse + attemptNum int + retryParams RetryParams + operationName string + expectShouldRetry bool + expectWaitDuration bool // Whether we expect a non-zero wait duration + minExpectedDuration int // Minimum expected duration in ms + }{ + { + name: "no_error_no_retry", + err: nil, + response: nil, + attemptNum: 0, + retryParams: RetryParams{MaxRetry: 3, MinWaitInMs: 50}, + operationName: "Test", + expectShouldRetry: false, + expectWaitDuration: false, + }, + { + name: "generic_error_retries", + err: errors.New("network error"), + response: nil, + attemptNum: 0, + retryParams: RetryParams{MaxRetry: 3, MinWaitInMs: 50}, + operationName: "Test", + expectShouldRetry: true, + expectWaitDuration: true, + minExpectedDuration: 50, + }, + { + name: "connection_error_retries", + err: errors.New("connection refused"), + response: nil, + attemptNum: 0, + retryParams: RetryParams{MaxRetry: 3, MinWaitInMs: 100}, + operationName: "Test", + expectShouldRetry: true, + expectWaitDuration: true, + minExpectedDuration: 100, + }, + { + name: "below_max_attempts", + err: errors.New("network error"), + response: nil, + attemptNum: 2, + retryParams: RetryParams{MaxRetry: 5, MinWaitInMs: 50}, + operationName: "Test", + expectShouldRetry: true, + expectWaitDuration: true, + minExpectedDuration: 50, + }, + { + name: "high_attempt_number", + err: errors.New("timeout"), + response: nil, + attemptNum: 10, + retryParams: RetryParams{MaxRetry: 15, MinWaitInMs: 50}, + operationName: "Test", + expectShouldRetry: true, + expectWaitDuration: true, + minExpectedDuration: 50, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { + return makeResp(200, `{"ok":true}`, nil), nil + }}, &tc.retryParams) + + executor := NewAPIExecutor(client).(*apiExecutor) + shouldRetry, waitDuration := executor.determineRetry( + tc.err, + tc.response, + tc.attemptNum, + tc.retryParams, + tc.operationName, + ) + + assert.Equal(t, tc.expectShouldRetry, shouldRetry) + if tc.expectWaitDuration { + assert.Greater(t, waitDuration.Milliseconds(), int64(tc.minExpectedDuration-1)) + } else { + assert.Equal(t, int64(0), waitDuration.Milliseconds()) + } + }) + } +} + +func TestBuildPath_EdgeCases(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + template string + params map[string]string + expectedPath string + }{ + { + name: "path_with_no_placeholders", + template: "/api/v1/stores", + params: map[string]string{"store_id": "123"}, + expectedPath: "/api/v1/stores", + }, + { + name: "placeholder_not_in_params", + template: "/stores/{store_id}", + params: map[string]string{"other_id": "123"}, + expectedPath: "/stores/{store_id}", + }, + { + name: "multiple_slashes_preserved", + template: "/stores//{store_id}//check", + params: map[string]string{ + "store_id": "123", + }, + expectedPath: "/stores//123//check", + }, + { + name: "placeholder_at_start", + template: "{store_id}/check", + params: map[string]string{ + "store_id": "123", + }, + expectedPath: "123/check", + }, + { + name: "placeholder_at_end", + template: "/stores/{store_id}", + params: map[string]string{ + "store_id": "123", + }, + expectedPath: "/stores/123", + }, + { + name: "adjacent_placeholders", + template: "/api/{version}{store_id}", + params: map[string]string{ + "version": "v1", + "store_id": "123", + }, + expectedPath: "/api/v1123", + }, + { + name: "placeholder_with_underscores_and_numbers", + template: "/stores/{store_id_1}/models/{model_id_2}", + params: map[string]string{ + "store_id_1": "abc", + "model_id_2": "xyz", + }, + expectedPath: "/stores/abc/models/xyz", + }, + { + name: "url_encoded_value_with_percent", + template: "/items/{id}", + params: map[string]string{ + "id": "100%", + }, + expectedPath: "/items/100%25", + }, + { + name: "value_with_curly_braces", + template: "/items/{id}", + params: map[string]string{ + "id": "{test}", + }, + expectedPath: "/items/%7Btest%7D", + }, + { + name: "value_with_plus_sign", + template: "/search/{query}", + params: map[string]string{ + "query": "hello+world", + }, + expectedPath: "/search/hello+world", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := buildPath(tc.template, tc.params) + assert.Equal(t, tc.expectedPath, result) + }) + } +} + +func TestPrepareHeaders_EdgeCases(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + customHeaders map[string]string + checkHeader string + expectedValue string + }{ + { + name: "empty_string_header_value", + customHeaders: map[string]string{ + "X-Empty": "", + }, + checkHeader: "X-Empty", + expectedValue: "", + }, + { + name: "header_with_special_characters", + customHeaders: map[string]string{ + "X-Special": "value with spaces and !@#$%", + }, + checkHeader: "X-Special", + expectedValue: "value with spaces and !@#$%", + }, + { + name: "very_long_header_value", + customHeaders: map[string]string{ + "X-Long": strings.Repeat("a", 1000), + }, + checkHeader: "X-Long", + expectedValue: strings.Repeat("a", 1000), + }, + { + name: "header_with_unicode", + customHeaders: map[string]string{ + "X-Unicode": "Hello δΈ–η•Œ 🌍", + }, + checkHeader: "X-Unicode", + expectedValue: "Hello δΈ–η•Œ 🌍", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := prepareHeaders(tc.customHeaders) + assert.Equal(t, tc.expectedValue, result[tc.checkHeader]) + }) + } +} + +func TestAPIExecutorResponse_Fields(t *testing.T) { + t.Parallel() + + t.Run("all_fields_populated", func(t *testing.T) { + t.Parallel() + + httpResp := &http.Response{ + StatusCode: 201, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "X-Request-ID": []string{"req-123"}, + "Content-Length": []string{"100"}, + }, + } + body := []byte(`{"created":true,"id":"abc123"}`) + + resp := makeAPIExecutorResponse(httpResp, body) + + assert.Equal(t, 201, resp.StatusCode) + assert.Equal(t, body, resp.Body) + assert.Equal(t, httpResp, resp.HTTPResponse) + assert.Equal(t, httpResp.Header, resp.Headers) + // Note: Header comparison validates that all headers are preserved + assert.Contains(t, resp.Headers["Content-Type"], "application/json") + assert.Contains(t, resp.Headers["X-Request-ID"], "req-123") + }) + + t.Run("access_body_directly", func(t *testing.T) { + t.Parallel() + + body := []byte(`{"data":"test"}`) + resp := makeAPIExecutorResponse(&http.Response{StatusCode: 200, Header: http.Header{}}, body) + + assert.Equal(t, `{"data":"test"}`, string(resp.Body)) + }) + + t.Run("response_with_redirect_status", func(t *testing.T) { + t.Parallel() + + httpResp := &http.Response{ + StatusCode: 302, + Header: http.Header{ + "Location": []string{"/new-location"}, + }, + } + body := []byte{} + + resp := makeAPIExecutorResponse(httpResp, body) + + assert.Equal(t, 302, resp.StatusCode) + assert.Equal(t, "/new-location", resp.Headers.Get("Location")) + assert.Empty(t, resp.Body) + }) +} + +func TestAPIExecutorRequestBuilder_Chaining(t *testing.T) { + t.Parallel() + + t.Run("complete_chain", func(t *testing.T) { + t.Parallel() + + req := NewAPIExecutorRequestBuilder("ComplexOp", "POST", "/stores/{store_id}/check"). + WithPathParameter("store_id", "store-123"). + WithPathParameter("model_id", "model-456"). // Extra param + WithQueryParameter("expand", "true"). + WithQueryParameter("limit", "10"). + WithHeader("Authorization", "Bearer token"). + WithHeader("X-API-Version", "v1"). + WithBody(map[string]string{"user": "user:anne"}). + Build() + + assert.Equal(t, "ComplexOp", req.OperationName) + assert.Equal(t, "POST", req.Method) + assert.Equal(t, "/stores/{store_id}/check", req.Path) + assert.Equal(t, "store-123", req.PathParameters["store_id"]) + assert.Equal(t, "model-456", req.PathParameters["model_id"]) + assert.Equal(t, "true", req.QueryParameters.Get("expand")) + assert.Equal(t, "10", req.QueryParameters.Get("limit")) + assert.Equal(t, "Bearer token", req.Headers["Authorization"]) + assert.Equal(t, "v1", req.Headers["X-API-Version"]) + assert.NotNil(t, req.Body) + }) + + t.Run("empty_chain", func(t *testing.T) { + t.Parallel() + + req := NewAPIExecutorRequestBuilder("Empty", "GET", "/empty").Build() + + assert.Equal(t, "Empty", req.OperationName) + assert.Equal(t, "GET", req.Method) + assert.Equal(t, "/empty", req.Path) + assert.NotNil(t, req.PathParameters) + assert.NotNil(t, req.QueryParameters) + assert.NotNil(t, req.Headers) + assert.Nil(t, req.Body) + }) + + t.Run("build_multiple_times", func(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Multi", "GET", "/test") + builder.WithPathParameter("key", "value1") + + req1 := builder.Build() + builder.WithPathParameter("key", "value2") + req2 := builder.Build() + + // Both builds should reflect the current state + assert.Equal(t, "value2", req1.PathParameters["key"]) + assert.Equal(t, "value2", req2.PathParameters["key"]) + }) +} + +func TestAPIExecutorRequestBuilder_Overrides(t *testing.T) { + t.Parallel() + + t.Run("path_parameters_replacement", func(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithPathParameter("id", "1") + builder.WithPathParameter("name", "test") + + newParams := map[string]string{ + "id": "2", + "different": "value", + } + builder.WithPathParameters(newParams) + + req := builder.Build() + + assert.Equal(t, "2", req.PathParameters["id"]) + assert.Equal(t, "value", req.PathParameters["different"]) + _, hasName := req.PathParameters["name"] + assert.False(t, hasName, "name parameter should be replaced") + }) + + t.Run("query_parameters_replacement", func(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithQueryParameter("page", "1") + builder.WithQueryParameter("limit", "10") + + newParams := url.Values{} + newParams.Add("page", "2") + newParams.Add("sort", "asc") + + builder.WithQueryParameters(newParams) + + req := builder.Build() + + assert.Equal(t, "2", req.QueryParameters.Get("page")) + assert.Equal(t, "asc", req.QueryParameters.Get("sort")) + assert.Empty(t, req.QueryParameters.Get("limit"), "limit should be replaced") + }) + + t.Run("headers_replacement", func(t *testing.T) { + t.Parallel() + + builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") + + builder.WithHeader("X-Old", "old") + builder.WithHeader("X-Keep", "keep") + + newHeaders := map[string]string{ + "X-New": "new", + } + + builder.WithHeaders(newHeaders) + + req := builder.Build() + + assert.Equal(t, "new", req.Headers["X-New"]) + _, hasOld := req.Headers["X-Old"] + assert.False(t, hasOld, "X-Old should be replaced") + _, hasKeep := req.Headers["X-Keep"] + assert.False(t, hasKeep, "X-Keep should be replaced") + }) +} + +func TestValidateRequest_AllFieldCombinations(t *testing.T) { + t.Parallel() + + t.Run("only_operation_name", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + OperationName: "Test", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "method is required") + }) + + t.Run("only_method", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + Method: "GET", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "operationName is required") + }) + + t.Run("only_path", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + Path: "/test", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "operationName is required") + }) + + t.Run("operation_name_and_method", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + OperationName: "Test", + Method: "GET", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "path is required") + }) + + t.Run("operation_name_and_path", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + OperationName: "Test", + Path: "/test", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "method is required") + }) + + t.Run("method_and_path", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + Method: "GET", + Path: "/test", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "operationName is required") + }) + + t.Run("all_required_fields_with_optional_fields", func(t *testing.T) { + t.Parallel() + + err := validateRequest(APIExecutorRequest{ + OperationName: "Test", + Method: "POST", + Path: "/test", + PathParameters: map[string]string{"id": "123"}, + QueryParameters: url.Values{"page": []string{"1"}}, + Body: map[string]string{"data": "test"}, + Headers: map[string]string{"X-Test": "value"}, + }) + assert.NoError(t, err) + }) +} + +func TestBuildPath_SpecialCases(t *testing.T) { + t.Parallel() + + t.Run("empty_template", func(t *testing.T) { + t.Parallel() + + result := buildPath("", map[string]string{"id": "123"}) + assert.Equal(t, "", result) + }) + + t.Run("template_with_only_placeholder", func(t *testing.T) { + t.Parallel() + + result := buildPath("{id}", map[string]string{"id": "123"}) + assert.Equal(t, "123", result) + }) + + t.Run("nested_braces", func(t *testing.T) { + t.Parallel() + + result := buildPath("/api/{{id}}", map[string]string{"id": "123"}) + assert.Equal(t, "/api/{123}", result) + }) + + t.Run("placeholder_with_dash", func(t *testing.T) { + t.Parallel() + + result := buildPath("/stores/{store-id}", map[string]string{"store-id": "123"}) + assert.Equal(t, "/stores/123", result) + }) + + t.Run("empty_params_map", func(t *testing.T) { + t.Parallel() + + result := buildPath("/stores/{store_id}", map[string]string{}) + assert.Equal(t, "/stores/{store_id}", result) + }) + + t.Run("value_with_equals_sign", func(t *testing.T) { + t.Parallel() + + result := buildPath("/query/{q}", map[string]string{"q": "key=value"}) + assert.Contains(t, result, "=") + }) + + t.Run("value_with_ampersand", func(t *testing.T) { + t.Parallel() + + result := buildPath("/query/{q}", map[string]string{"q": "a&b"}) + assert.Contains(t, result, "&") + }) +} diff --git a/utils_test.go b/utils_test.go index 09e55e2..dc416b0 100644 --- a/utils_test.go +++ b/utils_test.go @@ -236,3 +236,153 @@ func TestToPtrVsPtrFunctions(t *testing.T) { t.Run(tc.name, tc.testFunc) } } + +func TestValidatePathParameter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + paramName string + paramValue string + expectError bool + errorMsg string + }{ + { + name: "valid_non_empty_string", + paramName: "storeId", + paramValue: "01ARZ3NDEKTSV4RRFFQ69G5FAV", + expectError: false, + }, + { + name: "empty_string_returns_error", + paramName: "storeId", + paramValue: "", + expectError: true, + errorMsg: "storeId is required and must be specified", + }, + { + name: "whitespace_string_is_valid", + paramName: "modelId", + paramValue: " ", + expectError: false, + }, + { + name: "empty_string_different_parameter_name", + paramName: "authorizationModelId", + paramValue: "", + expectError: true, + errorMsg: "authorizationModelId is required and must be specified", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := validatePathParameter(tc.paramName, tc.paramValue) + + if tc.expectError { + require.Error(t, err) + assert.Equal(t, tc.errorMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestValidateParameter(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + paramName string + paramValue interface{} + expectError bool + errorMsg string + }{ + { + name: "valid_string_value", + paramName: "body", + paramValue: "test", + expectError: false, + }, + { + name: "valid_int_value", + paramName: "pageSize", + paramValue: 10, + expectError: false, + }, + { + name: "valid_struct_value", + paramName: "request", + paramValue: struct{ Name string }{Name: "test"}, + expectError: false, + }, + { + name: "valid_pointer_value", + paramName: "options", + paramValue: ToPtr("value"), + expectError: false, + }, + { + name: "valid_slice_value", + paramName: "items", + paramValue: []string{"a", "b", "c"}, + expectError: false, + }, + { + name: "valid_map_value", + paramName: "metadata", + paramValue: map[string]string{"key": "value"}, + expectError: false, + }, + { + name: "valid_empty_string_is_not_nil", + paramName: "body", + paramValue: "", + expectError: false, + }, + { + name: "valid_zero_int_is_not_nil", + paramName: "count", + paramValue: 0, + expectError: false, + }, + { + name: "valid_false_bool_is_not_nil", + paramName: "enabled", + paramValue: false, + expectError: false, + }, + { + name: "nil_value_returns_error", + paramName: "body", + paramValue: nil, + expectError: true, + errorMsg: "body is required and must be specified", + }, + { + name: "nil_value_different_parameter_name", + paramName: "request", + paramValue: nil, + expectError: true, + errorMsg: "request is required and must be specified", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := validateParameter(tc.paramName, tc.paramValue) + + if tc.expectError { + require.Error(t, err) + assert.Equal(t, tc.errorMsg, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} From bce12073a2df4966e2d2320a941b1e6f7561da94 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Wed, 19 Nov 2025 10:13:17 -0500 Subject: [PATCH 3/7] chore(docs): fixed Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aff600c..0684d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ ## [Unreleased](https://github.com/openfga/go-sdk/compare/v0.7.3...HEAD) +- feat: add support for StreamedListObjects endpoint (#252) - feat: add a generic API Executor `fgaClient.GetAPIExecutor()` to allow calling any OpenFGA API method. See [Calling Other Endpoints](./README.md#calling-other-endpoints) for more. - feat: add generic `ToPtr[T any](v T) *T` function for creating pointers to any type - deprecation: `PtrBool`, `PtrInt`, `PtrInt32`, `PtrInt64`, `PtrFloat32`, `PtrFloat64`, `PtrString`, and `PtrTime` are now deprecated in favor of the generic `ToPtr` function -- feat: add a top-level makefile in go-sdk to simplify running tests and linters: (#250) -- feat: add support for StreamedListObjects endpoint (#252) +- chore: add a top-level makefile in go-sdk to simplify running tests and linters: (#250) ## v0.7.3 From fa58c97c4adb31fd947098a08529a265b9f399a4 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Wed, 19 Nov 2025 20:43:59 -0500 Subject: [PATCH 4/7] fix: return error if not all path parameters are passed in --- README.md | 12 ++-- api_executor.go | 5 ++ api_executor_test.go | 134 ++++++++++++++++++++--------------- example/example1/example1.go | 6 +- utils_test.go | 8 +-- 5 files changed, 94 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index f9eb47a..7b14c95 100644 --- a/README.md +++ b/README.md @@ -1096,7 +1096,7 @@ data, err := fgaClient.WriteAssertions(context.Background()). ### Calling Other Endpoints In certain cases you may want to call other APIs not yet wrapped by the SDK. You can do so by using the `APIExecutor` available from the `fgaClient`. -The `APIExecutor` allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the operation name, HTTP method, path, parameters, body, and headers, while still taking into account the configuration and handling authentication, telemetry, retry and error handling. +The `APIExecutor` allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the operation name, HTTP method, path, parameters, body, and headers, while still honoring the client configuration (authentication, telemetry, retries, and error handling). This is useful when: @@ -1123,10 +1123,10 @@ requestBody := map[string]interface{}{ // Build the request request := openfga.NewAPIExecutorRequestBuilder("CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint"). WithPathParameter("store_id", storeID). - WithQueryParameter("page_size", "20"). - WithQueryParameter("continuation_token", "eyJwayI6..."). + WithQueryParameter("page_size", "20"). + WithQueryParameter("continuation_token", "eyJwayI6..."). WithBody(requestBody). - WithHeader("X-Experimental-Feature", "enabled"). + WithHeader("X-Experimental-Feature", "enabled"). Build() ``` @@ -1164,7 +1164,7 @@ type CustomEndpointResponse struct { var customEndpointResponse CustomEndpointResponse // Get raw response decoded into CustomEndpointResponse struct -rawResponse, err := executor.Execute(ctx, request, &customEndpointResponse) // Pass pointer to struct for decoding +rawResponse, err := executor.ExecuteWithDecode(ctx, request, &customEndpointResponse) // Pass pointer to struct for decoding if err != nil { log.Fatalf("Custom endpoint failed: %v", err) @@ -1175,7 +1175,7 @@ fmt.Printf("Response: %+v\n", customEndpointResponse) // You can access fields like headers, status code, etc. from rawResponse: fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) fmt.Printf("Headers: %+v\n", rawResponse.Headers) -```` +``` ### Retries diff --git a/api_executor.go b/api_executor.go index a2a2b64..951aa23 100644 --- a/api_executor.go +++ b/api_executor.go @@ -274,6 +274,11 @@ func (e *apiExecutor) executeInternal(ctx context.Context, request APIExecutorRe // Build request parameters path := buildPath(request.Path, request.PathParameters) + + if strings.Contains(path, "{") || strings.Contains(path, "}") { + return nil, reportError("not all path parameters were provided for path: %s", path) + } + headerParams := prepareHeaders(request.Headers) queryParams := request.QueryParameters if queryParams == nil { diff --git a/api_executor_test.go b/api_executor_test.go index 42a6c1c..17e222c 100644 --- a/api_executor_test.go +++ b/api_executor_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/openfga/go-sdk/internal/constants" + "github.com/openfga/go-sdk/internal/constants" ) // Test helpers @@ -50,7 +50,7 @@ func newTestClient(t *testing.T, rt http.RoundTripper, retry *RetryParams) *APIC // Tests func TestValidateRequest(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -137,7 +137,7 @@ func TestValidateRequest(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(tc.request) @@ -152,7 +152,7 @@ func TestValidateRequest(t *testing.T) { } func TestBuildPath(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -242,7 +242,7 @@ func TestBuildPath(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath(tc.template, tc.params) assert.Equal(t, tc.expectedPath, result) @@ -251,7 +251,7 @@ func TestBuildPath(t *testing.T) { } func TestPrepareHeaders(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -346,7 +346,7 @@ func TestPrepareHeaders(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() result := prepareHeaders(tc.customHeaders) assert.Equal(t, tc.expectedHeaders, result) @@ -355,7 +355,7 @@ func TestPrepareHeaders(t *testing.T) { } func TestMakeAPIExecutorResponse(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -415,7 +415,7 @@ func TestMakeAPIExecutorResponse(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() result := makeAPIExecutorResponse(tc.httpResponse, tc.body) @@ -429,10 +429,10 @@ func TestMakeAPIExecutorResponse(t *testing.T) { } func TestAPIExecutorRequestBuilder_NilMaps(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("with_path_parameter_initializes_nil_map", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := &APIExecutorRequestBuilder{ request: APIExecutorRequest{ @@ -447,7 +447,7 @@ func TestAPIExecutorRequestBuilder_NilMaps(t *testing.T) { }) t.Run("with_query_parameter_initializes_nil_map", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := &APIExecutorRequestBuilder{ request: APIExecutorRequest{ @@ -462,7 +462,7 @@ func TestAPIExecutorRequestBuilder_NilMaps(t *testing.T) { }) t.Run("with_header_initializes_nil_map", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := &APIExecutorRequestBuilder{ request: APIExecutorRequest{ @@ -478,7 +478,7 @@ func TestAPIExecutorRequestBuilder_NilMaps(t *testing.T) { } func TestAPIExecutorRequestBuilder_MultipleQueryValues(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -496,7 +496,7 @@ func TestAPIExecutorRequestBuilder_MultipleQueryValues(t *testing.T) { } func TestAPIExecutorRequestBuilder_PathParameterOverwrite(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/stores/{store_id}") @@ -509,7 +509,7 @@ func TestAPIExecutorRequestBuilder_PathParameterOverwrite(t *testing.T) { } func TestAPIExecutorRequestBuilder_QueryParameterReplace(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -528,7 +528,7 @@ func TestAPIExecutorRequestBuilder_QueryParameterReplace(t *testing.T) { } func TestAPIExecutorRequestBuilder_HeaderReplace(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -550,7 +550,7 @@ func TestAPIExecutorRequestBuilder_HeaderReplace(t *testing.T) { } func TestAPIExecutorRequestBuilder_BodyTypes(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -583,7 +583,7 @@ func TestAPIExecutorRequestBuilder_BodyTypes(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "POST", "/test") builder.WithBody(tc.body) @@ -595,10 +595,10 @@ func TestAPIExecutorRequestBuilder_BodyTypes(t *testing.T) { } func TestNewAPIExecutor(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("creates_executor_with_valid_client", func(t *testing.T) { - t.Parallel() + t.Parallel() client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { return makeResp(200, `{"ok":true}`, nil), nil @@ -610,7 +610,7 @@ func TestNewAPIExecutor(t *testing.T) { }) t.Run("executor_can_execute_request", func(t *testing.T) { - t.Parallel() + t.Parallel() client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { return makeResp(200, `{"message":"ok"}`, map[string]string{"Content-Type": "application/json"}), nil @@ -627,10 +627,28 @@ func TestNewAPIExecutor(t *testing.T) { assert.NotNil(t, resp) assert.Equal(t, 200, resp.StatusCode) }) + + t.Run("executor_error_on_missing_path_params", func(t *testing.T) { + t.Parallel() + + client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { + return makeResp(200, `{"message":"ok"}`, map[string]string{"Content-Type": "application/json"}), nil + }}, nil) + + executor := NewAPIExecutor(client) + resp, err := executor.Execute(context.Background(), APIExecutorRequest{ + OperationName: "Test", + Method: "GET", + Path: "/stores/{store_id}/test", + }) + + assert.Error(t, err) + assert.Nil(t, resp) + }) } func TestAPIExecutor_GetRetryParams(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -669,7 +687,7 @@ func TestAPIExecutor_GetRetryParams(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { return makeResp(200, `{"ok":true}`, nil), nil @@ -685,7 +703,7 @@ func TestAPIExecutor_GetRetryParams(t *testing.T) { } func TestAPIExecutor_DetermineRetry(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -756,7 +774,7 @@ func TestAPIExecutor_DetermineRetry(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() client := newTestClient(t, &testRoundTripper{fn: func(r *http.Request) (*http.Response, error) { return makeResp(200, `{"ok":true}`, nil), nil @@ -782,7 +800,7 @@ func TestAPIExecutor_DetermineRetry(t *testing.T) { } func TestBuildPath_EdgeCases(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -872,7 +890,7 @@ func TestBuildPath_EdgeCases(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath(tc.template, tc.params) assert.Equal(t, tc.expectedPath, result) @@ -881,7 +899,7 @@ func TestBuildPath_EdgeCases(t *testing.T) { } func TestPrepareHeaders_EdgeCases(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -925,7 +943,7 @@ func TestPrepareHeaders_EdgeCases(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() result := prepareHeaders(tc.customHeaders) assert.Equal(t, tc.expectedValue, result[tc.checkHeader]) @@ -934,10 +952,10 @@ func TestPrepareHeaders_EdgeCases(t *testing.T) { } func TestAPIExecutorResponse_Fields(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("all_fields_populated", func(t *testing.T) { - t.Parallel() + t.Parallel() httpResp := &http.Response{ StatusCode: 201, @@ -961,7 +979,7 @@ func TestAPIExecutorResponse_Fields(t *testing.T) { }) t.Run("access_body_directly", func(t *testing.T) { - t.Parallel() + t.Parallel() body := []byte(`{"data":"test"}`) resp := makeAPIExecutorResponse(&http.Response{StatusCode: 200, Header: http.Header{}}, body) @@ -970,7 +988,7 @@ func TestAPIExecutorResponse_Fields(t *testing.T) { }) t.Run("response_with_redirect_status", func(t *testing.T) { - t.Parallel() + t.Parallel() httpResp := &http.Response{ StatusCode: 302, @@ -989,10 +1007,10 @@ func TestAPIExecutorResponse_Fields(t *testing.T) { } func TestAPIExecutorRequestBuilder_Chaining(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("complete_chain", func(t *testing.T) { - t.Parallel() + t.Parallel() req := NewAPIExecutorRequestBuilder("ComplexOp", "POST", "/stores/{store_id}/check"). WithPathParameter("store_id", "store-123"). @@ -1017,7 +1035,7 @@ func TestAPIExecutorRequestBuilder_Chaining(t *testing.T) { }) t.Run("empty_chain", func(t *testing.T) { - t.Parallel() + t.Parallel() req := NewAPIExecutorRequestBuilder("Empty", "GET", "/empty").Build() @@ -1031,7 +1049,7 @@ func TestAPIExecutorRequestBuilder_Chaining(t *testing.T) { }) t.Run("build_multiple_times", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Multi", "GET", "/test") builder.WithPathParameter("key", "value1") @@ -1047,10 +1065,10 @@ func TestAPIExecutorRequestBuilder_Chaining(t *testing.T) { } func TestAPIExecutorRequestBuilder_Overrides(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("path_parameters_replacement", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -1072,7 +1090,7 @@ func TestAPIExecutorRequestBuilder_Overrides(t *testing.T) { }) t.Run("query_parameters_replacement", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -1093,7 +1111,7 @@ func TestAPIExecutorRequestBuilder_Overrides(t *testing.T) { }) t.Run("headers_replacement", func(t *testing.T) { - t.Parallel() + t.Parallel() builder := NewAPIExecutorRequestBuilder("Test", "GET", "/test") @@ -1117,10 +1135,10 @@ func TestAPIExecutorRequestBuilder_Overrides(t *testing.T) { } func TestValidateRequest_AllFieldCombinations(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("only_operation_name", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ OperationName: "Test", @@ -1130,7 +1148,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("only_method", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ Method: "GET", @@ -1140,7 +1158,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("only_path", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ Path: "/test", @@ -1150,7 +1168,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("operation_name_and_method", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ OperationName: "Test", @@ -1161,7 +1179,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("operation_name_and_path", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ OperationName: "Test", @@ -1172,7 +1190,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("method_and_path", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ Method: "GET", @@ -1183,7 +1201,7 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { }) t.Run("all_required_fields_with_optional_fields", func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateRequest(APIExecutorRequest{ OperationName: "Test", @@ -1199,52 +1217,52 @@ func TestValidateRequest_AllFieldCombinations(t *testing.T) { } func TestBuildPath_SpecialCases(t *testing.T) { - t.Parallel() + t.Parallel() t.Run("empty_template", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("", map[string]string{"id": "123"}) assert.Equal(t, "", result) }) t.Run("template_with_only_placeholder", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("{id}", map[string]string{"id": "123"}) assert.Equal(t, "123", result) }) t.Run("nested_braces", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("/api/{{id}}", map[string]string{"id": "123"}) assert.Equal(t, "/api/{123}", result) }) t.Run("placeholder_with_dash", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("/stores/{store-id}", map[string]string{"store-id": "123"}) assert.Equal(t, "/stores/123", result) }) t.Run("empty_params_map", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("/stores/{store_id}", map[string]string{}) assert.Equal(t, "/stores/{store_id}", result) }) t.Run("value_with_equals_sign", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("/query/{q}", map[string]string{"q": "key=value"}) assert.Contains(t, result, "=") }) t.Run("value_with_ampersand", func(t *testing.T) { - t.Parallel() + t.Parallel() result := buildPath("/query/{q}", map[string]string{"q": "a&b"}) assert.Contains(t, result, "&") diff --git a/example/example1/example1.go b/example/example1/example1.go index e4f1981..2f6e4de 100644 --- a/example/example1/example1.go +++ b/example/example1/example1.go @@ -275,8 +275,8 @@ func mainInner() error { } fmt.Printf("Allowed (with custom headers): %v\n", checkWithHeadersResponse.Allowed) - // Checking for access using a custom executor - fmt.Println("Checking for access with context") + // Checking for access using the API executor + fmt.Println("Checking for access using the API executor") // Get the API executor executor := fgaClient.GetAPIExecutor() @@ -287,7 +287,7 @@ func mainInner() error { WithHeader("X-Experimental-Feature", "enabled"). Build() - // custom executor + decoded response + // custom executor + decoded response var checkResponseCustomExecutorWithDecode openfga.CheckResponse _, err = executor.ExecuteWithDecode(ctx, customRequest, &checkResponseCustomExecutorWithDecode) if err != nil { diff --git a/utils_test.go b/utils_test.go index dc416b0..7240910 100644 --- a/utils_test.go +++ b/utils_test.go @@ -238,7 +238,7 @@ func TestToPtrVsPtrFunctions(t *testing.T) { } func TestValidatePathParameter(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -277,7 +277,7 @@ func TestValidatePathParameter(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() err := validatePathParameter(tc.paramName, tc.paramValue) @@ -292,7 +292,7 @@ func TestValidatePathParameter(t *testing.T) { } func TestValidateParameter(t *testing.T) { - t.Parallel() + t.Parallel() testCases := []struct { name string @@ -373,7 +373,7 @@ func TestValidateParameter(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - t.Parallel() + t.Parallel() err := validateParameter(tc.paramName, tc.paramValue) From 6247f5e9d27b88e891529d62d487539091b112ed Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 10 Dec 2025 21:39:45 +0530 Subject: [PATCH 5/7] fix: changelog and lint --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0684d58..eca2e2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## [Unreleased](https://github.com/openfga/go-sdk/compare/v0.7.3...HEAD) -- feat: add support for StreamedListObjects endpoint (#252) - feat: add a generic API Executor `fgaClient.GetAPIExecutor()` to allow calling any OpenFGA API method. See [Calling Other Endpoints](./README.md#calling-other-endpoints) for more. - feat: add generic `ToPtr[T any](v T) *T` function for creating pointers to any type - deprecation: `PtrBool`, `PtrInt`, `PtrInt32`, `PtrInt64`, `PtrFloat32`, `PtrFloat64`, `PtrString`, and `PtrTime` are now deprecated in favor of the generic `ToPtr` function From 0af064097256bc4276d8b85f9bf6734f3e486594 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 10 Dec 2025 21:44:01 +0530 Subject: [PATCH 6/7] fix: lint --- api_executor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_executor_test.go b/api_executor_test.go index 17e222c..052ec88 100644 --- a/api_executor_test.go +++ b/api_executor_test.go @@ -975,7 +975,7 @@ func TestAPIExecutorResponse_Fields(t *testing.T) { assert.Equal(t, httpResp.Header, resp.Headers) // Note: Header comparison validates that all headers are preserved assert.Contains(t, resp.Headers["Content-Type"], "application/json") - assert.Contains(t, resp.Headers["X-Request-ID"], "req-123") + assert.Contains(t, resp.Headers["X-Request-Id"], "req-123") }) t.Run("access_body_directly", func(t *testing.T) { From 2fc838e643d1906140b26353f2529caae28e633a Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 10 Dec 2025 21:44:59 +0530 Subject: [PATCH 7/7] fix: tests --- api_executor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_executor_test.go b/api_executor_test.go index 052ec88..9da1f6f 100644 --- a/api_executor_test.go +++ b/api_executor_test.go @@ -961,7 +961,7 @@ func TestAPIExecutorResponse_Fields(t *testing.T) { StatusCode: 201, Header: http.Header{ "Content-Type": []string{"application/json"}, - "X-Request-ID": []string{"req-123"}, + "X-Request-Id": []string{"req-123"}, "Content-Length": []string{"100"}, }, }