From 2fe058d1671dbdbd313bc7d6d3a8feacb96ab709 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:55:51 +0000 Subject: [PATCH 01/12] feat(go-sdk): add basic-auth-optional seed test fixture Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...e_errors_UnauthorizedRequestErrorBody.json | 13 + .../basic-auth-optional/.fern/metadata.json | 10 + .../.github/workflows/ci.yml | 62 ++ seed/go-sdk/basic-auth-optional/README.md | 199 +++++ .../basic-auth-optional/basicauth/client.go | 65 ++ .../basicauth/raw_client.go | 114 +++ .../basic-auth-optional/client/client.go | 33 + .../basic-auth-optional/client/client_test.go | 45 ++ .../basic-auth-optional/core/api_error.go | 47 ++ seed/go-sdk/basic-auth-optional/core/http.go | 15 + .../core/request_option.go | 139 ++++ .../dynamic-snippets/example0/snippet.go | 23 + .../dynamic-snippets/example1/snippet.go | 23 + .../dynamic-snippets/example2/snippet.go | 23 + .../dynamic-snippets/example3/snippet.go | 27 + .../dynamic-snippets/example4/snippet.go | 27 + .../dynamic-snippets/example5/snippet.go | 27 + .../dynamic-snippets/example6/snippet.go | 27 + .../go-sdk/basic-auth-optional/error_codes.go | 21 + seed/go-sdk/basic-auth-optional/errors.go | 44 ++ seed/go-sdk/basic-auth-optional/file_param.go | 41 + seed/go-sdk/basic-auth-optional/go.mod | 16 + seed/go-sdk/basic-auth-optional/go.sum | 12 + .../basic-auth-optional/internal/caller.go | 311 ++++++++ .../internal/caller_test.go | 705 ++++++++++++++++++ .../internal/error_decoder.go | 64 ++ .../internal/error_decoder_test.go | 59 ++ .../internal/explicit_fields.go | 116 +++ .../internal/explicit_fields_test.go | 645 ++++++++++++++++ .../internal/extra_properties.go | 141 ++++ .../internal/extra_properties_test.go | 228 ++++++ .../basic-auth-optional/internal/http.go | 71 ++ .../basic-auth-optional/internal/query.go | 358 +++++++++ .../internal/query_test.go | 395 ++++++++++ .../basic-auth-optional/internal/retrier.go | 239 ++++++ .../internal/retrier_test.go | 352 +++++++++ .../basic-auth-optional/internal/stringer.go | 13 + .../basic-auth-optional/internal/time.go | 165 ++++ .../option/request_option.go | 81 ++ seed/go-sdk/basic-auth-optional/pointer.go | 137 ++++ .../basic-auth-optional/pointer_test.go | 211 ++++++ seed/go-sdk/basic-auth-optional/reference.md | 105 +++ seed/go-sdk/basic-auth-optional/snippet.json | 26 + seed/go-sdk/basic-auth-optional/types.go | 94 +++ seed/go-sdk/basic-auth-optional/types_test.go | 153 ++++ .../basic-auth-optional/definition/api.yml | 12 + .../definition/basic-auth.yml | 39 + .../basic-auth-optional/definition/errors.yml | 11 + .../apis/basic-auth-optional/generators.yml | 22 + 49 files changed, 5806 insertions(+) create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json create mode 100644 seed/go-sdk/basic-auth-optional/.fern/metadata.json create mode 100644 seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml create mode 100644 seed/go-sdk/basic-auth-optional/README.md create mode 100644 seed/go-sdk/basic-auth-optional/basicauth/client.go create mode 100644 seed/go-sdk/basic-auth-optional/basicauth/raw_client.go create mode 100644 seed/go-sdk/basic-auth-optional/client/client.go create mode 100644 seed/go-sdk/basic-auth-optional/client/client_test.go create mode 100644 seed/go-sdk/basic-auth-optional/core/api_error.go create mode 100644 seed/go-sdk/basic-auth-optional/core/http.go create mode 100644 seed/go-sdk/basic-auth-optional/core/request_option.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go create mode 100644 seed/go-sdk/basic-auth-optional/error_codes.go create mode 100644 seed/go-sdk/basic-auth-optional/errors.go create mode 100644 seed/go-sdk/basic-auth-optional/file_param.go create mode 100644 seed/go-sdk/basic-auth-optional/go.mod create mode 100644 seed/go-sdk/basic-auth-optional/go.sum create mode 100644 seed/go-sdk/basic-auth-optional/internal/caller.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/caller_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/error_decoder.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/explicit_fields.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/extra_properties.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/http.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/query.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/query_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/retrier.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/retrier_test.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/stringer.go create mode 100644 seed/go-sdk/basic-auth-optional/internal/time.go create mode 100644 seed/go-sdk/basic-auth-optional/option/request_option.go create mode 100644 seed/go-sdk/basic-auth-optional/pointer.go create mode 100644 seed/go-sdk/basic-auth-optional/pointer_test.go create mode 100644 seed/go-sdk/basic-auth-optional/reference.md create mode 100644 seed/go-sdk/basic-auth-optional/snippet.json create mode 100644 seed/go-sdk/basic-auth-optional/types.go create mode 100644 seed/go-sdk/basic-auth-optional/types_test.go create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/api.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/errors.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/generators.yml diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json new file mode 100644 index 000000000000..f50ccac10d76 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/seed/go-sdk/basic-auth-optional/.fern/metadata.json b/seed/go-sdk/basic-auth-optional/.fern/metadata.json new file mode 100644 index 000000000000..1e0f7d8e54e4 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/.fern/metadata.json @@ -0,0 +1,10 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-go-sdk", + "generatorVersion": "latest", + "generatorConfig": { + "enableWireTests": false + }, + "originGitCommit": "DUMMY", + "sdkVersion": "v0.0.1" +} \ No newline at end of file diff --git a/seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml new file mode 100644 index 000000000000..1097e6a18acc --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.10.1 + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Setup wiremock server + run: | + PROJECT_NAME="wiremock-$(basename $(dirname $(pwd)) | tr -d '.')" + echo "PROJECT_NAME=$PROJECT_NAME" >> $GITHUB_ENV + if [ -f wiremock/docker-compose.test.yml ]; then + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml down + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml up -d + WIREMOCK_PORT=$(docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml port wiremock 8080 | cut -d: -f2) + echo "WIREMOCK_URL=http://localhost:$WIREMOCK_PORT" >> $GITHUB_ENV + fi + + - name: Test + run: go test ./... + + - name: Teardown wiremock server + if: always() + run: | + if [ -f wiremock/docker-compose.test.yml ]; then + docker compose -p "$PROJECT_NAME" -f wiremock/docker-compose.test.yml down + fi diff --git a/seed/go-sdk/basic-auth-optional/README.md b/seed/go-sdk/basic-auth-optional/README.md new file mode 100644 index 000000000000..38f9aa12a7b8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/README.md @@ -0,0 +1,199 @@ +# Seed Go Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FGo) + +The Seed Go library provides convenient access to the Seed APIs from Go. + +## Table of Contents + +- [Reference](#reference) +- [Usage](#usage) +- [Environments](#environments) +- [Errors](#errors) +- [Request Options](#request-options) +- [Advanced](#advanced) + - [Response Headers](#response-headers) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Explicit Null](#explicit-null) +- [Contributing](#contributing) + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```go +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBasicAuth( + "", + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} +``` + +## Environments + +You can choose between different environments by using the `option.WithBaseURL` option. You can configure any arbitrary base +URL, which is particularly useful in test environments. + +```go +client := client.NewClient( + option.WithBaseURL("https://example.com"), +) +``` + +## Errors + +Structured error types are returned from API calls that return non-success status codes. These errors are compatible +with the `errors.Is` and `errors.As` APIs, so you can access the error like so: + +```go +response, err := client.BasicAuth.PostWithBasicAuth(...) +if err != nil { + var apiError *core.APIError + if errors.As(err, apiError) { + // Do something with the API error ... + } + return err +} +``` + +## Request Options + +A variety of request options are included to adapt the behavior of the library, which includes configuring +authorization tokens, or providing your own instrumented `*http.Client`. + +These request options can either be +specified on the client so that they're applied on every request, or for an individual request, like so: + +> Providing your own `*http.Client` is recommended. Otherwise, the `http.DefaultClient` will be used, +> and your client will wait indefinitely for a response (unless the per-request, context-based timeout +> is used). + +```go +// Specify default options applied on every request. +client := client.NewClient( + option.WithToken(""), + option.WithHTTPClient( + &http.Client{ + Timeout: 5 * time.Second, + }, + ), +) + +// Specify options for an individual request. +response, err := client.BasicAuth.PostWithBasicAuth( + ..., + option.WithToken(""), +) +``` + +## Advanced + +### Response Headers + +You can access the raw HTTP response data by using the `WithRawResponse` field on the client. This is useful +when you need to examine the response headers received from the API call. (When the endpoint is paginated, +the raw HTTP response data will be included automatically in the Page response object.) + +```go +response, err := client.BasicAuth.WithRawResponse.PostWithBasicAuth(...) +if err != nil { + return err +} +fmt.Printf("Got response headers: %v", response.Header) +fmt.Printf("Got status code: %d", response.StatusCode) +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly +over the default exponential backoff. + +Use the `option.WithMaxAttempts` option to configure this behavior for the entire client or an individual request: + +```go +client := client.NewClient( + option.WithMaxAttempts(1), +) + +response, err := client.BasicAuth.PostWithBasicAuth( + ..., + option.WithMaxAttempts(1), +) +``` + +### Timeouts + +Setting a timeout for each individual request is as simple as using the standard context library. Setting a one second timeout for an individual API call looks like the following: + +```go +ctx, cancel := context.WithTimeout(ctx, time.Second) +defer cancel() + +response, err := client.BasicAuth.PostWithBasicAuth(ctx, ...) +``` + +### Explicit Null + +If you want to send the explicit `null` JSON value through an optional parameter, you can use the setters\ +that come with every object. Calling a setter method for a property will flip a bit in the `explicitFields` +bitfield for that setter's object; during serialization, any property with a flipped bit will have its +omittable status stripped, so zero or `nil` values will be sent explicitly rather than omitted altogether: + +```go +type ExampleRequest struct { + // An optional string parameter. + Name *string `json:"name,omitempty" url:"-"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` +} + +request := &ExampleRequest{} +request.SetName(nil) + +response, err := client.BasicAuth.PostWithBasicAuth(ctx, request, ...) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/go-sdk/basic-auth-optional/basicauth/client.go b/seed/go-sdk/basic-auth-optional/basicauth/client.go new file mode 100644 index 000000000000..f31be0b83313 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/basicauth/client.go @@ -0,0 +1,65 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauth + +import ( + context "context" + + core "github.com/basic-auth-optional/fern/core" + internal "github.com/basic-auth-optional/fern/internal" + option "github.com/basic-auth-optional/fern/option" +) + +type Client struct { + WithRawResponse *RawClient + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(options *core.RequestOptions) *Client { + return &Client{ + WithRawResponse: NewRawClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +// GET request with basic auth scheme +func (c *Client) GetWithBasicAuth( + ctx context.Context, + opts ...option.RequestOption, +) (bool, error) { + response, err := c.WithRawResponse.GetWithBasicAuth( + ctx, + opts..., + ) + if err != nil { + return false, err + } + return response.Body, nil +} + +// POST request with basic auth scheme +func (c *Client) PostWithBasicAuth( + ctx context.Context, + request any, + opts ...option.RequestOption, +) (bool, error) { + response, err := c.WithRawResponse.PostWithBasicAuth( + ctx, + request, + opts..., + ) + if err != nil { + return false, err + } + return response.Body, nil +} diff --git a/seed/go-sdk/basic-auth-optional/basicauth/raw_client.go b/seed/go-sdk/basic-auth-optional/basicauth/raw_client.go new file mode 100644 index 000000000000..b4aae98cd3e8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/basicauth/raw_client.go @@ -0,0 +1,114 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauth + +import ( + context "context" + http "net/http" + + fern "github.com/basic-auth-optional/fern" + core "github.com/basic-auth-optional/fern/core" + internal "github.com/basic-auth-optional/fern/internal" + option "github.com/basic-auth-optional/fern/option" +) + +type RawClient struct { + baseURL string + caller *internal.Caller + options *core.RequestOptions +} + +func NewRawClient(options *core.RequestOptions) *RawClient { + return &RawClient{ + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} + +func (r *RawClient) GetWithBasicAuth( + ctx context.Context, + opts ...option.RequestOption, +) (*core.Response[bool], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/basic-auth" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response bool + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + ErrorDecoder: internal.NewErrorDecoder(fern.ErrorCodes), + }, + ) + if err != nil { + return nil, err + } + return &core.Response[bool]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} + +func (r *RawClient) PostWithBasicAuth( + ctx context.Context, + request any, + opts ...option.RequestOption, +) (*core.Response[bool], error) { + options := core.NewRequestOptions(opts...) + baseURL := internal.ResolveBaseURL( + options.BaseURL, + r.baseURL, + "", + ) + endpointURL := baseURL + "/basic-auth" + headers := internal.MergeHeaders( + r.options.ToHeader(), + options.ToHeader(), + ) + var response bool + raw, err := r.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + Headers: headers, + MaxAttempts: options.MaxAttempts, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + ErrorDecoder: internal.NewErrorDecoder(fern.ErrorCodes), + }, + ) + if err != nil { + return nil, err + } + return &core.Response[bool]{ + StatusCode: raw.StatusCode, + Header: raw.Header, + Body: response, + }, nil +} diff --git a/seed/go-sdk/basic-auth-optional/client/client.go b/seed/go-sdk/basic-auth-optional/client/client.go new file mode 100644 index 000000000000..ad934300ce9a --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/client/client.go @@ -0,0 +1,33 @@ +// Code generated by Fern. DO NOT EDIT. + +package client + +import ( + basicauth "github.com/basic-auth-optional/fern/basicauth" + core "github.com/basic-auth-optional/fern/core" + internal "github.com/basic-auth-optional/fern/internal" + option "github.com/basic-auth-optional/fern/option" +) + +type Client struct { + BasicAuth *basicauth.Client + + options *core.RequestOptions + baseURL string + caller *internal.Caller +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + BasicAuth: basicauth.NewClient(options), + options: options, + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + } +} diff --git a/seed/go-sdk/basic-auth-optional/client/client_test.go b/seed/go-sdk/basic-auth-optional/client/client_test.go new file mode 100644 index 000000000000..a67e052e5add --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/client/client_test.go @@ -0,0 +1,45 @@ +// Code generated by Fern. DO NOT EDIT. + +package client + +import ( + option "github.com/basic-auth-optional/fern/option" + assert "github.com/stretchr/testify/assert" + http "net/http" + testing "testing" + time "time" +) + +func TestNewClient(t *testing.T) { + t.Run("default", func(t *testing.T) { + c := NewClient() + assert.Empty(t, c.baseURL) + }) + + t.Run("base url", func(t *testing.T) { + c := NewClient( + option.WithBaseURL("test.co"), + ) + assert.Equal(t, "test.co", c.baseURL) + }) + + t.Run("http client", func(t *testing.T) { + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + c := NewClient( + option.WithHTTPClient(httpClient), + ) + assert.Empty(t, c.baseURL) + }) + + t.Run("http header", func(t *testing.T) { + header := make(http.Header) + header.Set("X-API-Tenancy", "test") + c := NewClient( + option.WithHTTPHeader(header), + ) + assert.Empty(t, c.baseURL) + assert.Equal(t, "test", c.options.HTTPHeader.Get("X-API-Tenancy")) + }) +} diff --git a/seed/go-sdk/basic-auth-optional/core/api_error.go b/seed/go-sdk/basic-auth-optional/core/api_error.go new file mode 100644 index 000000000000..6168388541b4 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/core/api_error.go @@ -0,0 +1,47 @@ +package core + +import ( + "fmt" + "net/http" +) + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` + Header http.Header `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, header http.Header, err error) *APIError { + return &APIError{ + err: err, + Header: header, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} diff --git a/seed/go-sdk/basic-auth-optional/core/http.go b/seed/go-sdk/basic-auth-optional/core/http.go new file mode 100644 index 000000000000..92c435692940 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/core/http.go @@ -0,0 +1,15 @@ +package core + +import "net/http" + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// Response is an HTTP response from an HTTP client. +type Response[T any] struct { + StatusCode int + Header http.Header + Body T +} diff --git a/seed/go-sdk/basic-auth-optional/core/request_option.go b/seed/go-sdk/basic-auth-optional/core/request_option.go new file mode 100644 index 000000000000..0b0f3c2155c2 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/core/request_option.go @@ -0,0 +1,139 @@ +// Code generated by Fern. DO NOT EDIT. + +package core + +import ( + base64 "encoding/base64" + http "net/http" + url "net/url" +) + +// RequestOption adapts the behavior of the client or an individual request. +type RequestOption interface { + applyRequestOptions(*RequestOptions) +} + +// RequestOptions defines all of the possible request options. +// +// This type is primarily used by the generated code and is not meant +// to be used directly; use the option package instead. +type RequestOptions struct { + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + MaxAttempts uint + MaxBufSize int + Username string + Password string +} + +// NewRequestOptions returns a new *RequestOptions value. +// +// This function is primarily used by the generated code and is not meant +// to be used directly; use RequestOption instead. +func NewRequestOptions(opts ...RequestOption) *RequestOptions { + options := &RequestOptions{ + HTTPHeader: make(http.Header), + BodyProperties: make(map[string]interface{}), + QueryParameters: make(url.Values), + } + for _, opt := range opts { + opt.applyRequestOptions(options) + } + return options +} + +// ToHeader maps the configured request options into a http.Header used +// for the request(s). +func (r *RequestOptions) ToHeader() http.Header { + header := r.cloneHeader() + if r.Username != "" || r.Password != "" { + header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(r.Username+":"+r.Password))) + } + return header +} + +func (r *RequestOptions) cloneHeader() http.Header { + headers := r.HTTPHeader.Clone() + headers.Set("X-Fern-Language", "Go") + headers.Set("X-Fern-SDK-Name", "github.com/basic-auth-optional/fern") + headers.Set("X-Fern-SDK-Version", "v0.0.1") + headers.Set("User-Agent", "github.com/basic-auth-optional/fern/0.0.1") + return headers +} + +// BaseURLOption implements the RequestOption interface. +type BaseURLOption struct { + BaseURL string +} + +func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) { + opts.BaseURL = b.BaseURL +} + +// HTTPClientOption implements the RequestOption interface. +type HTTPClientOption struct { + HTTPClient HTTPClient +} + +func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPClient = h.HTTPClient +} + +// HTTPHeaderOption implements the RequestOption interface. +type HTTPHeaderOption struct { + HTTPHeader http.Header +} + +func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPHeader = h.HTTPHeader +} + +// BodyPropertiesOption implements the RequestOption interface. +type BodyPropertiesOption struct { + BodyProperties map[string]interface{} +} + +func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) { + opts.BodyProperties = b.BodyProperties +} + +// QueryParametersOption implements the RequestOption interface. +type QueryParametersOption struct { + QueryParameters url.Values +} + +func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) { + opts.QueryParameters = q.QueryParameters +} + +// MaxAttemptsOption implements the RequestOption interface. +type MaxAttemptsOption struct { + MaxAttempts uint +} + +func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxAttempts = m.MaxAttempts +} + +// MaxBufSizeOption implements the RequestOption interface. +type MaxBufSizeOption struct { + MaxBufSize int +} + +func (m *MaxBufSizeOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxBufSize = m.MaxBufSize +} + +// BasicAuthOption implements the RequestOption interface. +type BasicAuthOption struct { + Username string + Password string +} + +func (b *BasicAuthOption) applyRequestOptions(opts *RequestOptions) { + opts.Username = b.Username + opts.Password = b.Password +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go new file mode 100644 index 000000000000..3cf23a6c80a0 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + client.BasicAuth.GetWithBasicAuth( + context.TODO(), + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go new file mode 100644 index 000000000000..3cf23a6c80a0 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + client.BasicAuth.GetWithBasicAuth( + context.TODO(), + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go new file mode 100644 index 000000000000..3cf23a6c80a0 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go @@ -0,0 +1,23 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + client.BasicAuth.GetWithBasicAuth( + context.TODO(), + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go new file mode 100644 index 000000000000..e873a933abb8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go new file mode 100644 index 000000000000..e873a933abb8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go new file mode 100644 index 000000000000..e873a933abb8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go new file mode 100644 index 000000000000..e873a933abb8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go @@ -0,0 +1,27 @@ +package example + +import ( + context "context" + + client "github.com/basic-auth-optional/fern/client" + option "github.com/basic-auth-optional/fern/option" +) + +func do() { + client := client.NewClient( + option.WithBaseURL( + "https://api.fern.com", + ), + option.WithBasicAuth( + "", + "", + ), + ) + request := map[string]any{ + "key": "value", + } + client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} diff --git a/seed/go-sdk/basic-auth-optional/error_codes.go b/seed/go-sdk/basic-auth-optional/error_codes.go new file mode 100644 index 000000000000..a3e89f4ec571 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/error_codes.go @@ -0,0 +1,21 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauthoptional + +import ( + core "github.com/basic-auth-optional/fern/core" + internal "github.com/basic-auth-optional/fern/internal" +) + +var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ + 401: func(apiError *core.APIError) error { + return &UnauthorizedRequest{ + APIError: apiError, + } + }, + 400: func(apiError *core.APIError) error { + return &BadRequest{ + APIError: apiError, + } + }, +} diff --git a/seed/go-sdk/basic-auth-optional/errors.go b/seed/go-sdk/basic-auth-optional/errors.go new file mode 100644 index 000000000000..cf0fe91ad9fe --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/errors.go @@ -0,0 +1,44 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauthoptional + +import ( + json "encoding/json" + core "github.com/basic-auth-optional/fern/core" +) + +type BadRequest struct { + *core.APIError +} + +func (b *BadRequest) UnmarshalJSON(data []byte) error { + b.StatusCode = 400 + return nil +} + +func (b *BadRequest) MarshalJSON() ([]byte, error) { + return nil, nil +} + +type UnauthorizedRequest struct { + *core.APIError + Body *UnauthorizedRequestErrorBody +} + +func (u *UnauthorizedRequest) UnmarshalJSON(data []byte) error { + var body *UnauthorizedRequestErrorBody + if err := json.Unmarshal(data, &body); err != nil { + return err + } + u.StatusCode = 401 + u.Body = body + return nil +} + +func (u *UnauthorizedRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(u.Body) +} + +func (u *UnauthorizedRequest) Unwrap() error { + return u.APIError +} diff --git a/seed/go-sdk/basic-auth-optional/file_param.go b/seed/go-sdk/basic-auth-optional/file_param.go new file mode 100644 index 000000000000..ee41d1ece30b --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/file_param.go @@ -0,0 +1,41 @@ +package basicauthoptional + +import ( + "io" +) + +// FileParam is a file type suitable for multipart/form-data uploads. +type FileParam struct { + io.Reader + filename string + contentType string +} + +// FileParamOption adapts the behavior of the FileParam. No options are +// implemented yet, but this interface allows for future extensibility. +type FileParamOption interface { + apply() +} + +// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file +// upload endpoints accept a simple io.Reader, which is usually created by opening a file +// via os.Open. +// +// However, some endpoints require additional metadata about the file such as a specific +// Content-Type or custom filename. FileParam makes it easier to create the correct type +// signature for these endpoints. +func NewFileParam( + reader io.Reader, + filename string, + contentType string, + opts ...FileParamOption, +) *FileParam { + return &FileParam{ + Reader: reader, + filename: filename, + contentType: contentType, + } +} + +func (f *FileParam) Name() string { return f.filename } +func (f *FileParam) ContentType() string { return f.contentType } diff --git a/seed/go-sdk/basic-auth-optional/go.mod b/seed/go-sdk/basic-auth-optional/go.mod new file mode 100644 index 000000000000..8c580a08cdd9 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/go.mod @@ -0,0 +1,16 @@ +module github.com/basic-auth-optional/fern + +go 1.21 + +toolchain go1.23.8 + +require github.com/google/uuid v1.6.0 + +require github.com/stretchr/testify v1.8.4 + +require gopkg.in/yaml.v3 v3.0.1 // indirect + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/seed/go-sdk/basic-auth-optional/go.sum b/seed/go-sdk/basic-auth-optional/go.sum new file mode 100644 index 000000000000..fcca6d128057 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-sdk/basic-auth-optional/internal/caller.go b/seed/go-sdk/basic-auth-optional/internal/caller.go new file mode 100644 index 000000000000..665f4ecc2f50 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/caller.go @@ -0,0 +1,311 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/basic-auth-optional/fern/core" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" + contentTypeFormURLEncoded = "application/x-www-form-urlencoded" +) + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client core.HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client core.HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient core.HTTPClient = http.DefaultClient + if params.Client != nil { + httpClient = params.Client + } + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + return &Caller{ + client: httpClient, + retrier: NewRetrier(retryOptions...), + } +} + +// CallParams represents the parameters used to issue an API call. +type CallParams struct { + URL string + Method string + MaxAttempts uint + Headers http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + Client core.HTTPClient + Request interface{} + Response interface{} + ResponseIsOptional bool + ErrorDecoder ErrorDecoder +} + +// CallResponse is a parsed HTTP response from an API call. +type CallResponse struct { + StatusCode int + Header http.Header +} + +// Call issues an API call according to the given call parameters. +func (c *Caller) Call(ctx context.Context, params *CallParams) (*CallResponse, error) { + url := buildURL(params.URL, params.QueryParameters) + req, err := newRequest( + ctx, + url, + params.Method, + params.Headers, + params.Request, + params.BodyProperties, + ) + if err != nil { + return nil, err + } + + // If the call has been cancelled, don't issue the request. + if err := ctx.Err(); err != nil { + return nil, err + } + + client := c.client + if params.Client != nil { + // Use the HTTP client scoped to the request. + client = params.Client + } + + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + + resp, err := c.retrier.Run( + client.Do, + req, + params.ErrorDecoder, + retryOptions..., + ) + if err != nil { + return nil, err + } + + // Close the response body after we're done. + defer func() { _ = resp.Body.Close() }() + + // Check if the call was cancelled before we return the error + // associated with the call and/or unmarshal the response data. + if err := ctx.Err(); err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, decodeError(resp, params.ErrorDecoder) + } + + // Mutate the response parameter in-place. + if params.Response != nil { + if writer, ok := params.Response.(io.Writer); ok { + _, err = io.Copy(writer, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(params.Response) + } + if err != nil { + if err == io.EOF { + if params.ResponseIsOptional { + // The response is optional, so we should ignore the + // io.EOF error + return &CallResponse{ + StatusCode: resp.StatusCode, + Header: resp.Header, + }, nil + } + return nil, fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response) + } + return nil, err + } + } + + return &CallResponse{ + StatusCode: resp.StatusCode, + Header: resp.Header, + }, nil +} + +// buildURL constructs the final URL by appending the given query parameters (if any). +func buildURL( + url string, + queryParameters url.Values, +) string { + if len(queryParameters) == 0 { + return url + } + if strings.ContainsRune(url, '?') { + url += "&" + } else { + url += "?" + } + url += queryParameters.Encode() + return url +} + +// newRequest returns a new *http.Request with all of the fields +// required to issue the call. +func newRequest( + ctx context.Context, + url string, + method string, + endpointHeaders http.Header, + request interface{}, + bodyProperties map[string]interface{}, +) (*http.Request, error) { + // Determine the content type from headers, defaulting to JSON. + reqContentType := contentType + if endpointHeaders != nil { + if ct := endpointHeaders.Get(contentTypeHeader); ct != "" { + reqContentType = ct + } + } + requestBody, err := newRequestBody(request, bodyProperties, reqContentType) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return nil, err + } + req.Header.Set(contentTypeHeader, reqContentType) + for name, values := range endpointHeaders { + req.Header[name] = values + } + return req, nil +} + +// newRequestBody returns a new io.Reader that represents the HTTP request body. +func newRequestBody(request interface{}, bodyProperties map[string]interface{}, reqContentType string) (io.Reader, error) { + if isNil(request) { + if len(bodyProperties) == 0 { + return nil, nil + } + if reqContentType == contentTypeFormURLEncoded { + return newFormURLEncodedBody(bodyProperties), nil + } + requestBytes, err := json.Marshal(bodyProperties) + if err != nil { + return nil, err + } + return bytes.NewReader(requestBytes), nil + } + if body, ok := request.(io.Reader); ok { + return body, nil + } + // Handle form URL encoded content type. + if reqContentType == contentTypeFormURLEncoded { + return newFormURLEncodedRequestBody(request, bodyProperties) + } + requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties) + if err != nil { + return nil, err + } + return bytes.NewReader(requestBytes), nil +} + +// newFormURLEncodedBody returns a new io.Reader that represents a form URL encoded body +// from the given body properties map. +func newFormURLEncodedBody(bodyProperties map[string]interface{}) io.Reader { + values := url.Values{} + for key, val := range bodyProperties { + values.Set(key, fmt.Sprintf("%v", val)) + } + return strings.NewReader(values.Encode()) +} + +// newFormURLEncodedRequestBody returns a new io.Reader that represents a form URL encoded body +// from the given request struct and body properties. +func newFormURLEncodedRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) { + values := url.Values{} + // Marshal the request to JSON first to respect any custom MarshalJSON methods, + // then unmarshal into a map to extract the field values. + jsonBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + var jsonMap map[string]interface{} + if err := json.Unmarshal(jsonBytes, &jsonMap); err != nil { + return nil, err + } + // Convert the JSON map to form URL encoded values. + for key, val := range jsonMap { + if val == nil { + continue + } + values.Set(key, fmt.Sprintf("%v", val)) + } + // Add any extra body properties. + for key, val := range bodyProperties { + values.Set(key, fmt.Sprintf("%v", val)) + } + return strings.NewReader(values.Encode()), nil +} + +// decodeError decodes the error from the given HTTP response. Note that +// it's the caller's responsibility to close the response body. +func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { + if errorDecoder != nil { + // This endpoint has custom errors, so we'll + // attempt to unmarshal the error into a structured + // type based on the status code. + return errorDecoder(response.StatusCode, response.Header, response.Body) + } + // This endpoint doesn't have any custom error + // types, so we just read the body as-is, and + // put it into a normal error. + bytes, err := io.ReadAll(response.Body) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + // The error didn't have a response body, + // so all we can do is return an error + // with the status code. + return core.NewAPIError(response.StatusCode, response.Header, nil) + } + return core.NewAPIError(response.StatusCode, response.Header, errors.New(string(bytes))) +} + +// isNil is used to determine if the request value is equal to nil (i.e. an interface +// value that holds a nil concrete value is itself non-nil). +func isNil(value interface{}) bool { + if value == nil { + return true + } + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} diff --git a/seed/go-sdk/basic-auth-optional/internal/caller_test.go b/seed/go-sdk/basic-auth-optional/internal/caller_test.go new file mode 100644 index 000000000000..50b1ea8969d5 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/caller_test.go @@ -0,0 +1,705 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + + "github.com/basic-auth-optional/fern/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// InternalTestCase represents a single test case. +type InternalTestCase struct { + description string + + // Server-side assertions. + givePathSuffix string + giveMethod string + giveResponseIsOptional bool + giveHeader http.Header + giveErrorDecoder ErrorDecoder + giveRequest *InternalTestRequest + giveQueryParams url.Values + giveBodyProperties map[string]interface{} + + // Client-side assertions. + wantResponse *InternalTestResponse + wantError error +} + +// InternalTestRequest a simple request body. +type InternalTestRequest struct { + Id string `json:"id"` +} + +// InternalTestResponse a simple response body. +type InternalTestResponse struct { + Id string `json:"id"` + ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"` + QueryParameters url.Values `json:"queryParameters,omitempty"` +} + +// InternalTestNotFoundError represents a 404. +type InternalTestNotFoundError struct { + *core.APIError + + Message string `json:"message"` +} + +func TestCall(t *testing.T) { + tests := []*InternalTestCase{ + { + description: "GET success", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + Id: "123", + }, + }, + { + description: "GET success with query", + givePathSuffix: "?limit=1", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + Id: "123", + QueryParameters: url.Values{ + "limit": []string{"1"}, + }, + }, + }, + { + description: "GET not found", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &InternalTestRequest{ + Id: strconv.Itoa(http.StatusNotFound), + }, + giveErrorDecoder: newTestErrorDecoder(t), + wantError: &InternalTestNotFoundError{ + APIError: core.NewAPIError( + http.StatusNotFound, + http.Header{}, + errors.New(`{"message":"ID \"404\" not found"}`), + ), + }, + }, + { + description: "POST empty body", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: nil, + wantError: core.NewAPIError( + http.StatusBadRequest, + http.Header{}, + errors.New("invalid request"), + ), + }, + { + description: "POST optional response", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + giveResponseIsOptional: true, + }, + { + description: "POST API error", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &InternalTestRequest{ + Id: strconv.Itoa(http.StatusInternalServerError), + }, + wantError: core.NewAPIError( + http.StatusInternalServerError, + http.Header{}, + errors.New("failed to process request"), + ), + }, + { + description: "POST extra properties", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: new(InternalTestRequest), + giveBodyProperties: map[string]interface{}{ + "key": "value", + }, + wantResponse: &InternalTestResponse{ + ExtraBodyProperties: map[string]interface{}{ + "key": "value", + }, + }, + }, + { + description: "GET extra query parameters", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveQueryParams: url.Values{ + "extra": []string{"true"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + wantResponse: &InternalTestResponse{ + Id: "123", + QueryParameters: url.Values{ + "extra": []string{"true"}, + }, + }, + }, + { + description: "GET merge extra query parameters", + givePathSuffix: "?limit=1", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &InternalTestRequest{ + Id: "123", + }, + giveQueryParams: url.Values{ + "extra": []string{"true"}, + }, + wantResponse: &InternalTestResponse{ + Id: "123", + QueryParameters: url.Values{ + "limit": []string{"1"}, + "extra": []string{"true"}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + var ( + server = newTestServer(t, test) + client = server.Client() + ) + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL + test.givePathSuffix, + Method: test.giveMethod, + Headers: test.giveHeader, + BodyProperties: test.giveBodyProperties, + QueryParameters: test.giveQueryParams, + Request: test.giveRequest, + Response: &response, + ResponseIsOptional: test.giveResponseIsOptional, + ErrorDecoder: test.giveErrorDecoder, + }, + ) + if test.wantError != nil { + assert.EqualError(t, err, test.wantError.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +func TestMergeHeaders(t *testing.T) { + t.Run("both empty", func(t *testing.T) { + merged := MergeHeaders(make(http.Header), make(http.Header)) + assert.Empty(t, merged) + }) + + t.Run("empty left", func(t *testing.T) { + left := make(http.Header) + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("empty right", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.1") + + right := make(http.Header) + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("single value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.0") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) + + t.Run("multiple value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Versions", "0.0.0") + + right := make(http.Header) + right.Add("X-API-Versions", "0.0.1") + right.Add("X-API-Versions", "0.0.2") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions")) + }) + + t.Run("disjoint merge", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Tenancy", "test") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy")) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) +} + +// newTestServer returns a new *httptest.Server configured with the +// given test parameters. +func newTestServer(t *testing.T, tc *InternalTestCase) *httptest.Server { + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tc.giveMethod, r.Method) + assert.Equal(t, contentType, r.Header.Get(contentTypeHeader)) + for header, value := range tc.giveHeader { + assert.Equal(t, value, r.Header.Values(header)) + } + + request := new(InternalTestRequest) + + bytes, err := io.ReadAll(r.Body) + if tc.giveRequest == nil { + require.Empty(t, bytes) + w.WriteHeader(http.StatusBadRequest) + _, err = w.Write([]byte("invalid request")) + require.NoError(t, err) + return + } + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + + switch request.Id { + case strconv.Itoa(http.StatusNotFound): + notFoundError := &InternalTestNotFoundError{ + APIError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + Message: fmt.Sprintf("ID %q not found", request.Id), + } + bytes, err = json.Marshal(notFoundError) + require.NoError(t, err) + + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(bytes) + require.NoError(t, err) + return + + case strconv.Itoa(http.StatusInternalServerError): + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write([]byte("failed to process request")) + require.NoError(t, err) + return + } + + if tc.giveResponseIsOptional { + w.WriteHeader(http.StatusOK) + return + } + + extraBodyProperties := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties)) + delete(extraBodyProperties, "id") + + response := &InternalTestResponse{ + Id: request.Id, + ExtraBodyProperties: extraBodyProperties, + QueryParameters: r.URL.Query(), + } + bytes, err = json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(bytes) + require.NoError(t, err) + }, + ), + ) +} + +func TestIsNil(t *testing.T) { + t.Run("nil interface", func(t *testing.T) { + assert.True(t, isNil(nil)) + }) + + t.Run("nil pointer", func(t *testing.T) { + var ptr *string + assert.True(t, isNil(ptr)) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + s := "test" + assert.False(t, isNil(&s)) + }) + + t.Run("nil slice", func(t *testing.T) { + var slice []string + assert.True(t, isNil(slice)) + }) + + t.Run("non-nil slice", func(t *testing.T) { + slice := []string{} + assert.False(t, isNil(slice)) + }) + + t.Run("nil map", func(t *testing.T) { + var m map[string]string + assert.True(t, isNil(m)) + }) + + t.Run("non-nil map", func(t *testing.T) { + m := make(map[string]string) + assert.False(t, isNil(m)) + }) + + t.Run("string value", func(t *testing.T) { + assert.False(t, isNil("test")) + }) + + t.Run("empty string value", func(t *testing.T) { + assert.False(t, isNil("")) + }) + + t.Run("int value", func(t *testing.T) { + assert.False(t, isNil(42)) + }) + + t.Run("zero int value", func(t *testing.T) { + assert.False(t, isNil(0)) + }) + + t.Run("bool value", func(t *testing.T) { + assert.False(t, isNil(true)) + }) + + t.Run("false bool value", func(t *testing.T) { + assert.False(t, isNil(false)) + }) + + t.Run("struct value", func(t *testing.T) { + type testStruct struct { + Field string + } + assert.False(t, isNil(testStruct{Field: "test"})) + }) + + t.Run("empty struct value", func(t *testing.T) { + type testStruct struct { + Field string + } + assert.False(t, isNil(testStruct{})) + }) +} + +// newTestErrorDecoder returns an error decoder suitable for tests. +func newTestErrorDecoder(t *testing.T) func(int, http.Header, io.Reader) error { + return func(statusCode int, header http.Header, body io.Reader) error { + raw, err := io.ReadAll(body) + require.NoError(t, err) + + var ( + apiError = core.NewAPIError(statusCode, header, errors.New(string(raw))) + decoder = json.NewDecoder(bytes.NewReader(raw)) + ) + if statusCode == http.StatusNotFound { + value := new(InternalTestNotFoundError) + value.APIError = apiError + require.NoError(t, decoder.Decode(value)) + + return value + } + return apiError + } +} + +// FormURLEncodedTestRequest is a test struct for form URL encoding tests. +type FormURLEncodedTestRequest struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + GrantType string `json:"grant_type,omitempty"` + Scope *string `json:"scope,omitempty"` + NilPointer *string `json:"nil_pointer,omitempty"` +} + +func TestNewFormURLEncodedBody(t *testing.T) { + t.Run("simple key-value pairs", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "grant_type": "client_credentials", + } + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + + // Verify it's not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON, got: %s", bodyStr) + }) + + t.Run("special characters requiring URL encoding", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "value_with_space": "hello world", + "value_with_ampersand": "a&b", + "value_with_equals": "a=b", + "value_with_plus": "a+b", + } + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values are correctly decoded + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "hello world", values.Get("value_with_space")) + assert.Equal(t, "a&b", values.Get("value_with_ampersand")) + assert.Equal(t, "a=b", values.Get("value_with_equals")) + assert.Equal(t, "a+b", values.Get("value_with_plus")) + }) + + t.Run("empty map", func(t *testing.T) { + bodyProperties := map[string]interface{}{} + reader := newFormURLEncodedBody(bodyProperties) + body, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Empty(t, string(body)) + }) +} + +func TestNewFormURLEncodedRequestBody(t *testing.T) { + t.Run("struct with json tags", func(t *testing.T) { + scope := "read write" + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "client_credentials", + Scope: &scope, + NilPointer: nil, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Parse the body and verify values + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + assert.Equal(t, "read write", values.Get("scope")) + // nil_pointer should not be present (nil pointer with omitempty) + assert.Empty(t, values.Get("nil_pointer")) + + // Verify it's not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON, got: %s", bodyStr) + }) + + t.Run("struct with omitempty and zero values", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "", // empty string with omitempty should be omitted + Scope: nil, + NilPointer: nil, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + // grant_type should not be present (empty string with omitempty) + assert.Empty(t, values.Get("grant_type")) + assert.Empty(t, values.Get("scope")) + }) + + t.Run("struct with extra body properties", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + } + bodyProperties := map[string]interface{}{ + "extra_param": "extra_value", + } + reader, err := newFormURLEncodedRequestBody(request, bodyProperties) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "extra_value", values.Get("extra_param")) + }) + + t.Run("special characters in struct fields", func(t *testing.T) { + scope := "read&write=all+permissions" + request := &FormURLEncodedTestRequest{ + ClientID: "client with spaces", + ClientSecret: "secret&with=special+chars", + Scope: &scope, + } + reader, err := newFormURLEncodedRequestBody(request, nil) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "client with spaces", values.Get("client_id")) + assert.Equal(t, "secret&with=special+chars", values.Get("client_secret")) + assert.Equal(t, "read&write=all+permissions", values.Get("scope")) + }) +} + +func TestNewRequestBodyFormURLEncoded(t *testing.T) { + t.Run("selects form encoding when content-type is form-urlencoded", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + GrantType: "client_credentials", + } + reader, err := newRequestBody(request, nil, contentTypeFormURLEncoded) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Verify it's form-urlencoded, not JSON + bodyStr := string(body) + assert.False(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should not be JSON when Content-Type is form-urlencoded, got: %s", bodyStr) + + // Parse and verify values + values, err := url.ParseQuery(bodyStr) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + assert.Equal(t, "client_credentials", values.Get("grant_type")) + }) + + t.Run("selects JSON encoding when content-type is application/json", func(t *testing.T) { + request := &FormURLEncodedTestRequest{ + ClientID: "test_client_id", + ClientSecret: "test_client_secret", + } + reader, err := newRequestBody(request, nil, contentType) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + // Verify it's JSON + bodyStr := string(body) + assert.True(t, strings.HasPrefix(strings.TrimSpace(bodyStr), "{"), + "Body should be JSON when Content-Type is application/json, got: %s", bodyStr) + + // Parse and verify it's valid JSON + var parsed map[string]interface{} + err = json.Unmarshal(body, &parsed) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", parsed["client_id"]) + assert.Equal(t, "test_client_secret", parsed["client_secret"]) + }) + + t.Run("form encoding with body properties only (nil request)", func(t *testing.T) { + bodyProperties := map[string]interface{}{ + "client_id": "test_client_id", + "client_secret": "test_client_secret", + } + reader, err := newRequestBody(nil, bodyProperties, contentTypeFormURLEncoded) + require.NoError(t, err) + + body, err := io.ReadAll(reader) + require.NoError(t, err) + + values, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + assert.Equal(t, "test_client_id", values.Get("client_id")) + assert.Equal(t, "test_client_secret", values.Get("client_secret")) + }) +} diff --git a/seed/go-sdk/basic-auth-optional/internal/error_decoder.go b/seed/go-sdk/basic-auth-optional/internal/error_decoder.go new file mode 100644 index 000000000000..ebafe345fa1e --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/error_decoder.go @@ -0,0 +1,64 @@ +package internal + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/basic-auth-optional/fern/core" +) + +// ErrorCodes maps HTTP status codes to error constructors. +type ErrorCodes map[int]func(*core.APIError) error + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *core.APIError). +type ErrorDecoder func(statusCode int, header http.Header, body io.Reader) error + +// NewErrorDecoder returns a new ErrorDecoder backed by the given error codes. +// errorCodesOverrides is optional and will be merged with the default error codes, +// with overrides taking precedence. +func NewErrorDecoder(errorCodes ErrorCodes, errorCodesOverrides ...ErrorCodes) ErrorDecoder { + // Merge default error codes with overrides + mergedErrorCodes := make(ErrorCodes) + + // Start with default error codes + for statusCode, errorFunc := range errorCodes { + mergedErrorCodes[statusCode] = errorFunc + } + + // Apply overrides if provided + if len(errorCodesOverrides) > 0 && errorCodesOverrides[0] != nil { + for statusCode, errorFunc := range errorCodesOverrides[0] { + mergedErrorCodes[statusCode] = errorFunc + } + } + + return func(statusCode int, header http.Header, body io.Reader) error { + raw, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("failed to read error from response body: %w", err) + } + apiError := core.NewAPIError( + statusCode, + header, + errors.New(string(raw)), + ) + newErrorFunc, ok := mergedErrorCodes[statusCode] + if !ok { + // This status code isn't recognized, so we return + // the API error as-is. + return apiError + } + customError := newErrorFunc(apiError) + if err := json.NewDecoder(bytes.NewReader(raw)).Decode(customError); err != nil { + // If we fail to decode the error, we return the + // API error as-is. + return apiError + } + return customError + } +} diff --git a/seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go b/seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go new file mode 100644 index 000000000000..85b529f99ec1 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go @@ -0,0 +1,59 @@ +package internal + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "github.com/basic-auth-optional/fern/core" + "github.com/stretchr/testify/assert" +) + +func TestErrorDecoder(t *testing.T) { + decoder := NewErrorDecoder( + ErrorCodes{ + http.StatusNotFound: func(apiError *core.APIError) error { + return &InternalTestNotFoundError{APIError: apiError} + }, + }) + + tests := []struct { + description string + giveStatusCode int + giveHeader http.Header + giveBody string + wantError error + }{ + { + description: "unrecognized status code", + giveStatusCode: http.StatusInternalServerError, + giveHeader: http.Header{}, + giveBody: "Internal Server Error", + wantError: core.NewAPIError(http.StatusInternalServerError, http.Header{}, errors.New("Internal Server Error")), + }, + { + description: "not found with valid JSON", + giveStatusCode: http.StatusNotFound, + giveHeader: http.Header{}, + giveBody: `{"message": "Resource not found"}`, + wantError: &InternalTestNotFoundError{ + APIError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New(`{"message": "Resource not found"}`)), + Message: "Resource not found", + }, + }, + { + description: "not found with invalid JSON", + giveStatusCode: http.StatusNotFound, + giveHeader: http.Header{}, + giveBody: `Resource not found`, + wantError: core.NewAPIError(http.StatusNotFound, http.Header{}, errors.New("Resource not found")), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + assert.Equal(t, tt.wantError, decoder(tt.giveStatusCode, tt.giveHeader, bytes.NewReader([]byte(tt.giveBody)))) + }) + } +} diff --git a/seed/go-sdk/basic-auth-optional/internal/explicit_fields.go b/seed/go-sdk/basic-auth-optional/internal/explicit_fields.go new file mode 100644 index 000000000000..4bdf34fc2b7c --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/explicit_fields.go @@ -0,0 +1,116 @@ +package internal + +import ( + "math/big" + "reflect" + "strings" +) + +// HandleExplicitFields processes a struct to remove `omitempty` from +// fields that have been explicitly set (as indicated by their corresponding bit in explicitFields). +// Note that `marshaler` should be an embedded struct to avoid infinite recursion. +// Returns an interface{} that can be passed to json.Marshal. +func HandleExplicitFields(marshaler interface{}, explicitFields *big.Int) interface{} { + val := reflect.ValueOf(marshaler) + typ := reflect.TypeOf(marshaler) + + // Handle pointer types + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil + } + val = val.Elem() + typ = typ.Elem() + } + + // Only handle struct types + if val.Kind() != reflect.Struct { + return marshaler + } + + // Handle embedded struct pattern + var sourceVal reflect.Value + var sourceType reflect.Type + + // Check if this is an embedded struct pattern + if typ.NumField() == 1 && typ.Field(0).Anonymous { + // This is likely an embedded struct, get the embedded value + embeddedField := val.Field(0) + sourceVal = embeddedField + sourceType = embeddedField.Type() + } else { + // Regular struct + sourceVal = val + sourceType = typ + } + + // If no explicit fields set, use standard marshaling + if explicitFields == nil || explicitFields.Sign() == 0 { + return marshaler + } + + // Create a new struct type with modified tags + fields := make([]reflect.StructField, 0, sourceType.NumField()) + + for i := 0; i < sourceType.NumField(); i++ { + field := sourceType.Field(i) + + // Skip unexported fields and the explicitFields field itself + if !field.IsExported() || field.Name == "explicitFields" { + continue + } + + // Check if this field has been explicitly set + fieldBit := big.NewInt(1) + fieldBit.Lsh(fieldBit, uint(i)) + if big.NewInt(0).And(explicitFields, fieldBit).Sign() != 0 { + // Remove omitempty from the json tag + tag := field.Tag.Get("json") + if tag != "" && tag != "-" { + // Parse the json tag, remove omitempty from options + parts := strings.Split(tag, ",") + if len(parts) > 1 { + var newParts []string + newParts = append(newParts, parts[0]) // Keep the field name + for _, part := range parts[1:] { + if strings.TrimSpace(part) != "omitempty" { + newParts = append(newParts, part) + } + } + tag = strings.Join(newParts, ",") + } + + // Reconstruct the struct tag + newTag := `json:"` + tag + `"` + if urlTag := field.Tag.Get("url"); urlTag != "" { + newTag += ` url:"` + urlTag + `"` + } + + field.Tag = reflect.StructTag(newTag) + } + } + + fields = append(fields, field) + } + + // Create new struct type with modified tags + newType := reflect.StructOf(fields) + newVal := reflect.New(newType).Elem() + + // Copy field values from original struct to new struct + fieldIndex := 0 + for i := 0; i < sourceType.NumField(); i++ { + originalField := sourceType.Field(i) + + // Skip unexported fields and the explicitFields field itself + if !originalField.IsExported() || originalField.Name == "explicitFields" { + continue + } + + originalValue := sourceVal.Field(i) + newVal.Field(fieldIndex).Set(originalValue) + fieldIndex++ + } + + return newVal.Interface() +} diff --git a/seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go b/seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go new file mode 100644 index 000000000000..f44beec447d6 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go @@ -0,0 +1,645 @@ +package internal + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testExplicitFieldsStruct struct { + Name *string `json:"name,omitempty"` + Code *string `json:"code,omitempty"` + Count *int `json:"count,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Tags []string `json:"tags,omitempty"` + unexported string `json:"-"` //nolint:unused + explicitFields *big.Int `json:"-"` +} + +var ( + testFieldName = big.NewInt(1 << 0) + testFieldCode = big.NewInt(1 << 1) + testFieldCount = big.NewInt(1 << 2) + testFieldEnabled = big.NewInt(1 << 3) + testFieldTags = big.NewInt(1 << 4) +) + +func (t *testExplicitFieldsStruct) require(field *big.Int) { + if t.explicitFields == nil { + t.explicitFields = big.NewInt(0) + } + t.explicitFields.Or(t.explicitFields, field) +} + +func (t *testExplicitFieldsStruct) SetName(name *string) { + t.Name = name + t.require(testFieldName) +} + +func (t *testExplicitFieldsStruct) SetCode(code *string) { + t.Code = code + t.require(testFieldCode) +} + +func (t *testExplicitFieldsStruct) SetCount(count *int) { + t.Count = count + t.require(testFieldCount) +} + +func (t *testExplicitFieldsStruct) SetEnabled(enabled *bool) { + t.Enabled = enabled + t.require(testFieldEnabled) +} + +func (t *testExplicitFieldsStruct) SetTags(tags []string) { + t.Tags = tags + t.require(testFieldTags) +} + +func (t *testExplicitFieldsStruct) MarshalJSON() ([]byte, error) { + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*t), + } + return json.Marshal(HandleExplicitFields(marshaler, t.explicitFields)) +} + +type testStructWithoutExplicitFields struct { + Name *string `json:"name,omitempty"` + Code *string `json:"code,omitempty"` +} + +func TestHandleExplicitFields(t *testing.T) { + tests := []struct { + desc string + giveInput interface{} + wantBytes []byte + wantError string + }{ + { + desc: "nil input", + giveInput: nil, + wantBytes: []byte(`null`), + }, + { + desc: "non-struct input", + giveInput: "string", + wantBytes: []byte(`"string"`), + }, + { + desc: "slice input", + giveInput: []string{"a", "b"}, + wantBytes: []byte(`["a","b"]`), + }, + { + desc: "map input", + giveInput: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "struct without explicitFields field", + giveInput: &testStructWithoutExplicitFields{ + Name: stringPtr("test"), + Code: nil, + }, + wantBytes: []byte(`{"name":"test"}`), + }, + { + desc: "struct with no explicit fields set", + giveInput: &testExplicitFieldsStruct{ + Name: stringPtr("test"), + Code: nil, + }, + wantBytes: []byte(`{"name":"test"}`), + }, + { + desc: "struct with explicit nil field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null}`), + }, + { + desc: "struct with explicit non-nil field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetName(stringPtr("explicit")) + s.SetCode(stringPtr("also-explicit")) + return s + }(), + wantBytes: []byte(`{"name":"explicit","code":"also-explicit"}`), + }, + { + desc: "struct with mixed explicit and implicit fields", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Count: intPtr(42), + } + s.SetCode(nil) // explicit nil + return s + }(), + wantBytes: []byte(`{"name":"implicit","code":null,"count":42}`), + }, + { + desc: "struct with multiple explicit nil fields", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + s.SetCount(nil) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null,"count":null}`), + }, + { + desc: "struct with slice field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{ + Tags: []string{"tag1", "tag2"}, + } + s.SetTags(nil) // explicit nil slice + return s + }(), + wantBytes: []byte(`{"tags":null}`), + }, + { + desc: "struct with boolean field", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetEnabled(boolPtr(false)) // explicit false + return s + }(), + wantBytes: []byte(`{"enabled":false}`), + }, + { + desc: "struct with all fields explicit", + giveInput: func() *testExplicitFieldsStruct { + s := &testExplicitFieldsStruct{} + s.SetName(stringPtr("test")) + s.SetCode(nil) + s.SetCount(intPtr(0)) + s.SetEnabled(boolPtr(false)) + s.SetTags([]string{}) + return s + }(), + wantBytes: []byte(`{"name":"test","code":null,"count":0,"enabled":false,"tags":[]}`), + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var explicitFields *big.Int + if s, ok := tt.giveInput.(*testExplicitFieldsStruct); ok { + explicitFields = s.explicitFields + } + bytes, err := json.Marshal(HandleExplicitFields(tt.giveInput, explicitFields)) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.JSONEq(t, string(tt.wantBytes), string(bytes)) + + // Verify it's valid JSON + var value interface{} + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestHandleExplicitFieldsCustomMarshaler(t *testing.T) { + t.Run("custom marshaler with explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("test-code")) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes)) + }) + + t.Run("custom marshaler with no explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Code: stringPtr("also-implicit"), + } + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsPointerHandling(t *testing.T) { + t.Run("nil pointer", func(t *testing.T) { + var s *testExplicitFieldsStruct + bytes, err := json.Marshal(HandleExplicitFields(s, nil)) + require.NoError(t, err) + assert.Equal(t, []byte(`null`), bytes) + }) + + t.Run("pointer to struct", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + + bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields)) + require.NoError(t, err) + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsEmbeddedStruct(t *testing.T) { + t.Run("embedded struct with explicit fields", func(t *testing.T) { + // Create a struct similar to what MarshalJSON creates + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("test-code")) + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should include both explicit fields (name as null, code as "test-code") + assert.JSONEq(t, `{"name":null,"code":"test-code"}`, string(bytes)) + }) + + t.Run("embedded struct with no explicit fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("implicit"), + Code: stringPtr("also-implicit"), + } + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should only include non-nil fields (omitempty behavior) + assert.JSONEq(t, `{"name":"implicit","code":"also-implicit"}`, string(bytes)) + }) + + t.Run("embedded struct with mixed fields", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Count: intPtr(42), // implicit field + } + s.SetName(nil) // explicit nil + s.SetCode(stringPtr("explicit")) // explicit value + + type embed testExplicitFieldsStruct + var marshaler = struct { + embed + }{ + embed: embed(*s), + } + + bytes, err := json.Marshal(HandleExplicitFields(marshaler, s.explicitFields)) + require.NoError(t, err) + // Should include explicit null, explicit value, and implicit value + assert.JSONEq(t, `{"name":null,"code":"explicit","count":42}`, string(bytes)) + }) +} + +func TestHandleExplicitFieldsTagHandling(t *testing.T) { + type testStructWithComplexTags struct { + Field1 *string `json:"field1,omitempty" url:"field1,omitempty"` + Field2 *string `json:"field2,omitempty,string" url:"field2"` + Field3 *string `json:"-"` + Field4 *string `json:"field4"` + explicitFields *big.Int `json:"-"` + } + + s := &testStructWithComplexTags{ + Field1: stringPtr("test1"), + Field4: stringPtr("test4"), + explicitFields: big.NewInt(1), // Only first field is explicit + } + + bytes, err := json.Marshal(HandleExplicitFields(s, s.explicitFields)) + require.NoError(t, err) + + // Field1 should have omitempty removed, Field2 should keep omitempty, Field4 should be included + assert.JSONEq(t, `{"field1":"test1","field4":"test4"}`, string(bytes)) +} + +// Test types for nested struct explicit fields testing +type testNestedStruct struct { + NestedName *string `json:"nested_name,omitempty"` + NestedCode *string `json:"nested_code,omitempty"` + explicitFields *big.Int `json:"-"` +} + +type testParentStruct struct { + ParentName *string `json:"parent_name,omitempty"` + Nested *testNestedStruct `json:"nested,omitempty"` + explicitFields *big.Int `json:"-"` +} + +var ( + nestedFieldName = big.NewInt(1 << 0) + nestedFieldCode = big.NewInt(1 << 1) +) + +var ( + parentFieldName = big.NewInt(1 << 0) + parentFieldNested = big.NewInt(1 << 1) +) + +func (n *testNestedStruct) require(field *big.Int) { + if n.explicitFields == nil { + n.explicitFields = big.NewInt(0) + } + n.explicitFields.Or(n.explicitFields, field) +} + +func (n *testNestedStruct) SetNestedName(name *string) { + n.NestedName = name + n.require(nestedFieldName) +} + +func (n *testNestedStruct) SetNestedCode(code *string) { + n.NestedCode = code + n.require(nestedFieldCode) +} + +func (n *testNestedStruct) MarshalJSON() ([]byte, error) { + type embed testNestedStruct + var marshaler = struct { + embed + }{ + embed: embed(*n), + } + return json.Marshal(HandleExplicitFields(marshaler, n.explicitFields)) +} + +func (p *testParentStruct) require(field *big.Int) { + if p.explicitFields == nil { + p.explicitFields = big.NewInt(0) + } + p.explicitFields.Or(p.explicitFields, field) +} + +func (p *testParentStruct) SetParentName(name *string) { + p.ParentName = name + p.require(parentFieldName) +} + +func (p *testParentStruct) SetNested(nested *testNestedStruct) { + p.Nested = nested + p.require(parentFieldNested) +} + +func (p *testParentStruct) MarshalJSON() ([]byte, error) { + type embed testParentStruct + var marshaler = struct { + embed + }{ + embed: embed(*p), + } + return json.Marshal(HandleExplicitFields(marshaler, p.explicitFields)) +} + +func TestHandleExplicitFieldsNestedStruct(t *testing.T) { + tests := []struct { + desc string + setupFunc func() *testParentStruct + wantBytes []byte + }{ + { + desc: "nested struct with explicit nil in nested object", + setupFunc: func() *testParentStruct { + nested := &testNestedStruct{ + NestedName: stringPtr("implicit-nested"), + } + nested.SetNestedCode(nil) // explicit nil + + return &testParentStruct{ + ParentName: stringPtr("implicit-parent"), + Nested: nested, + } + }, + wantBytes: []byte(`{"parent_name":"implicit-parent","nested":{"nested_name":"implicit-nested","nested_code":null}}`), + }, + { + desc: "parent with explicit nil nested struct", + setupFunc: func() *testParentStruct { + parent := &testParentStruct{ + ParentName: stringPtr("implicit-parent"), + } + parent.SetNested(nil) // explicit nil nested struct + return parent + }, + wantBytes: []byte(`{"parent_name":"implicit-parent","nested":null}`), + }, + { + desc: "all explicit fields in nested structure", + setupFunc: func() *testParentStruct { + nested := &testNestedStruct{} + nested.SetNestedName(stringPtr("explicit-nested")) + nested.SetNestedCode(nil) // explicit nil + + parent := &testParentStruct{} + parent.SetParentName(nil) // explicit nil + parent.SetNested(nested) // explicit nested struct + + return parent + }, + wantBytes: []byte(`{"parent_name":null,"nested":{"nested_name":"explicit-nested","nested_code":null}}`), + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + parent := tt.setupFunc() + bytes, err := parent.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, string(tt.wantBytes), string(bytes)) + + // Verify it's valid JSON + var value interface{} + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +// Test for setter method documentation and behavior +func TestSetterMethodsDocumentation(t *testing.T) { + t.Run("setter prevents omitempty for nil values", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set nil - this should prevent omitempty + s.SetName(nil) + s.SetCode(nil) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Both fields should be included as null, not omitted + assert.JSONEq(t, `{"name":null,"code":null}`, string(bytes)) + }) + + t.Run("setter prevents omitempty for empty slice", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set empty slice + s.SetTags([]string{}) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Empty slice should be included as [], not omitted + assert.JSONEq(t, `{"tags":[]}`, string(bytes)) + }) + + t.Run("setter prevents omitempty for zero values", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Use setter to explicitly set zero values + s.SetCount(intPtr(0)) + s.SetEnabled(boolPtr(false)) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Zero values should be included, not omitted + assert.JSONEq(t, `{"count":0,"enabled":false}`, string(bytes)) + }) + + t.Run("direct assignment is omitted when nil", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: nil, // Direct assignment, not using setter + Code: nil, // Direct assignment, not using setter + } + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Fields not set via setter should be omitted when nil + assert.JSONEq(t, `{}`, string(bytes)) + }) + + t.Run("mix of setter and direct assignment", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("direct"), // Direct assignment + Count: intPtr(42), // Direct assignment + } + s.SetCode(nil) // Setter with nil + s.SetEnabled(boolPtr(false)) // Setter with zero value + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Direct assignments included if non-nil, setter fields always included + assert.JSONEq(t, `{"name":"direct","code":null,"count":42,"enabled":false}`, string(bytes)) + }) +} + +// Test for complex scenarios with multiple setters +func TestComplexSetterScenarios(t *testing.T) { + t.Run("multiple setter calls on same field", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + + // Call setter multiple times - last one should win + s.SetName(stringPtr("first")) + s.SetName(stringPtr("second")) + s.SetName(nil) // Final value is nil + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Should serialize the last set value (nil) + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) + + t.Run("setter after direct assignment", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("direct"), + } + + // Override with setter + s.SetName(nil) + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // Setter should mark field as explicit, so nil is serialized + assert.JSONEq(t, `{"name":null}`, string(bytes)) + }) + + t.Run("all fields set via setters", func(t *testing.T) { + s := &testExplicitFieldsStruct{} + s.SetName(nil) + s.SetCode(stringPtr("")) // Empty string + s.SetCount(intPtr(0)) // Zero + s.SetEnabled(boolPtr(false)) // False + s.SetTags(nil) // Nil slice + + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + // All fields should be present even with nil/zero values + assert.JSONEq(t, `{"name":null,"code":"","count":0,"enabled":false,"tags":null}`, string(bytes)) + }) +} + +// Test for backwards compatibility +func TestBackwardsCompatibility(t *testing.T) { + t.Run("struct without setters behaves normally", func(t *testing.T) { + s := &testStructWithoutExplicitFields{ + Name: stringPtr("test"), + Code: nil, // This should be omitted + } + + bytes, err := json.Marshal(s) + require.NoError(t, err) + + // Without setters, omitempty works normally + assert.JSONEq(t, `{"name":"test"}`, string(bytes)) + }) + + t.Run("struct with explicit fields works with standard json.Marshal", func(t *testing.T) { + s := &testExplicitFieldsStruct{ + Name: stringPtr("test"), + } + s.SetCode(nil) + + // Using the custom MarshalJSON + bytes, err := s.MarshalJSON() + require.NoError(t, err) + + assert.JSONEq(t, `{"name":"test","code":null}`, string(bytes)) + }) +} + +// Helper functions +func stringPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} + +func boolPtr(b bool) *bool { + return &b +} diff --git a/seed/go-sdk/basic-auth-optional/internal/extra_properties.go b/seed/go-sdk/basic-auth-optional/internal/extra_properties.go new file mode 100644 index 000000000000..540c3fd89eeb --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/extra_properties.go @@ -0,0 +1,141 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go b/seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go new file mode 100644 index 000000000000..aa2510ee5121 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go @@ -0,0 +1,228 @@ +package internal + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-sdk/basic-auth-optional/internal/http.go b/seed/go-sdk/basic-auth-optional/internal/http.go new file mode 100644 index 000000000000..77863752bb58 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/http.go @@ -0,0 +1,71 @@ +package internal + +import ( + "fmt" + "net/http" + "net/url" + "reflect" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// ResolveBaseURL resolves the base URL from the given arguments, +// preferring the first non-empty value. +func ResolveBaseURL(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. Pointer arguments are dereferenced before processing. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + // Dereference the argument if it's a pointer + value := dereferenceArg(arg) + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", value))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// dereferenceArg dereferences a pointer argument if necessary, returning the underlying value. +// If the argument is not a pointer or is nil, it returns the argument as-is. +func dereferenceArg(arg interface{}) interface{} { + if arg == nil { + return arg + } + + v := reflect.ValueOf(arg) + + // Keep dereferencing until we get to a non-pointer value or hit nil + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + return v.Interface() +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} diff --git a/seed/go-sdk/basic-auth-optional/internal/query.go b/seed/go-sdk/basic-auth-optional/internal/query.go new file mode 100644 index 000000000000..9b567f7a5563 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/query.go @@ -0,0 +1,358 @@ +package internal + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google/uuid" +) + +// RFC3339Milli is a time format string for RFC 3339 with millisecond precision. +// Go's time.RFC3339 omits fractional seconds and time.RFC3339Nano trims trailing +// zeros, so neither produces the fixed ".000" millisecond suffix that many APIs expect. +const RFC3339Milli = "2006-01-02T15:04:05.000Z07:00" + +var ( + bytesType = reflect.TypeOf([]byte{}) + queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() + timeType = reflect.TypeOf(time.Time{}) + uuidType = reflect.TypeOf(uuid.UUID{}) +) + +// QueryEncoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type QueryEncoder interface { + EncodeQueryValues(key string, v *url.Values) error +} + +// prepareValue handles common validation and unwrapping logic for both functions +func prepareValue(v interface{}) (reflect.Value, url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return reflect.Value{}, values, nil + } + val = val.Elem() + } + + if v == nil { + return reflect.Value{}, values, nil + } + + if val.Kind() != reflect.Struct { + return reflect.Value{}, nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + if err != nil { + return reflect.Value{}, nil, err + } + + return val, values, nil +} + +// QueryValues encodes url.Values from request objects. +// +// Note: This type is inspired by Google's query encoding library, but +// supports far less customization and is tailored to fit this SDK's use case. +// +// Ref: https://github.com/google/go-querystring +func QueryValues(v interface{}) (url.Values, error) { + _, values, err := prepareValue(v) + return values, err +} + +// QueryValuesWithDefaults encodes url.Values from request objects +// and default values, merging the defaults into the request. +// It's expected that the values of defaults are wire names. +func QueryValuesWithDefaults(v interface{}, defaults map[string]interface{}) (url.Values, error) { + val, values, err := prepareValue(v) + if err != nil { + return values, err + } + if !val.IsValid() { + return values, nil + } + + // apply defaults to zero-value fields directly on the original struct + valType := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := valType.Field(i) + fieldName := fieldType.Name + + if fieldType.PkgPath != "" && !fieldType.Anonymous { + // Skip unexported fields. + continue + } + + // check if field is zero value and we have a default for it + if field.CanSet() && field.IsZero() { + tag := fieldType.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + wireName, _ := parseTag(tag) + if wireName == "" { + wireName = fieldName + } + if defaultVal, exists := defaults[wireName]; exists { + values.Set(wireName, valueString(reflect.ValueOf(defaultVal), tagOptions{}, reflect.StructField{})) + } + } + } + + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Skip unexported fields. + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + + name, opts := parseTag(tag) + if name == "" { + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(queryEncoderType) { + // If sv is a nil pointer and the custom encoder is defined on a non-pointer + // method receiver, set sv to the zero value of the underlying type + if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(QueryEncoder) + if err := m.EncodeQueryValues(name, &values); err != nil { + return err + } + continue + } + + // Recursively dereference pointers, but stop at nil pointers. + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType { + values.Add(name, valueString(sv, opts, sf)) + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + if sv.Len() == 0 { + // Skip if slice or array is empty. + continue + } + for i := 0; i < sv.Len(); i++ { + value := sv.Index(i) + if isStructPointer(value) && !value.IsNil() { + if err := reflectValue(values, value.Elem(), name); err != nil { + return err + } + } else { + values.Add(name, valueString(value, opts, sf)) + } + } + continue + } + + if sv.Kind() == reflect.Map { + if err := reflectMap(values, sv, name); err != nil { + return err + } + continue + } + + if sv.Kind() == reflect.Struct { + if err := reflectValue(values, sv, name); err != nil { + return err + } + continue + } + + values.Add(name, valueString(sv, opts, sf)) + } + + return nil +} + +// reflectMap handles map types specifically, generating query parameters in the format key[mapkey]=value +func reflectMap(values url.Values, val reflect.Value, scope string) error { + if val.IsNil() { + return nil + } + + iter := val.MapRange() + for iter.Next() { + k := iter.Key() + v := iter.Value() + + key := fmt.Sprint(k.Interface()) + paramName := scope + "[" + key + "]" + + for v.Kind() == reflect.Ptr { + if v.IsNil() { + break + } + v = v.Elem() + } + + for v.Kind() == reflect.Interface { + v = v.Elem() + } + + if v.Kind() == reflect.Map { + if err := reflectMap(values, v, paramName); err != nil { + return err + } + continue + } + + if v.Kind() == reflect.Struct { + if err := reflectValue(values, v, paramName); err != nil { + return err + } + continue + } + + if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { + if v.Len() == 0 { + continue + } + for i := 0; i < v.Len(); i++ { + value := v.Index(i) + if isStructPointer(value) && !value.IsNil() { + if err := reflectValue(values, value.Elem(), paramName); err != nil { + return err + } + } else { + values.Add(paramName, valueString(value, tagOptions{}, reflect.StructField{})) + } + } + continue + } + + values.Add(paramName, valueString(v, tagOptions{}, reflect.StructField{})) + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if format := sf.Tag.Get("format"); format == "date" { + return t.Format("2006-01-02") + } + return t.Format(RFC3339Milli) + } + + if v.Type() == uuidType { + u := v.Interface().(uuid.UUID) + return u.String() + } + + if v.Type() == bytesType { + b := v.Interface().([]byte) + return base64.StdEncoding.EncodeToString(b) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + type zeroable interface { + IsZero() bool + } + + if !v.IsZero() { + if z, ok := v.Interface().(zeroable); ok { + return z.IsZero() + } + } + + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer: + return false + } + + return false +} + +// isStructPointer returns true if the given reflect.Value is a pointer to a struct. +func isStructPointer(v reflect.Value) bool { + return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/seed/go-sdk/basic-auth-optional/internal/query_test.go b/seed/go-sdk/basic-auth-optional/internal/query_test.go new file mode 100644 index 000000000000..5b463e297350 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/query_test.go @@ -0,0 +1,395 @@ +package internal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueryValues(t *testing.T) { + t.Run("empty optional", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Empty(t, values) + }) + + t.Run("empty required", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Equal(t, "required=", values.Encode()) + }) + + t.Run("allow multiple", func(t *testing.T) { + type example struct { + Values []string `json:"values" url:"values"` + } + + values, err := QueryValues( + &example{ + Values: []string{"foo", "bar", "baz"}, + }, + ) + require.NoError(t, err) + assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode()) + }) + + t.Run("nested object", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + nestedValue := "nestedValue" + values, err := QueryValues( + &example{ + Required: "requiredValue", + Nested: &nested{ + Value: &nestedValue, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode()) + }) + + t.Run("url unspecified", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("url ignored", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound" url:"-"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("datetime", func(t *testing.T) { + type example struct { + DateTime time.Time `json:"dateTime" url:"dateTime"` + } + + values, err := QueryValues( + &example{ + DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56.000Z", values.Encode()) + }) + + t.Run("date", func(t *testing.T) { + type example struct { + Date time.Time `json:"date" url:"date" format:"date"` + } + + values, err := QueryValues( + &example{ + Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "date=1994-03-16", values.Encode()) + }) + + t.Run("optional time", func(t *testing.T) { + type example struct { + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("omitempty with non-pointer zero value", func(t *testing.T) { + type enum string + + type example struct { + Enum enum `json:"enum,omitempty" url:"enum,omitempty"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("object array", func(t *testing.T) { + type object struct { + Key string `json:"key" url:"key"` + Value string `json:"value" url:"value"` + } + type example struct { + Objects []*object `json:"objects,omitempty" url:"objects,omitempty"` + } + + values, err := QueryValues( + &example{ + Objects: []*object{ + { + Key: "hello", + Value: "world", + }, + { + Key: "foo", + Value: "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode()) + }) + + t.Run("map", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "foo": "bar", + "baz": "qux", + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Bbaz%5D=qux&metadata%5Bfoo%5D=bar", values.Encode()) + }) + + t.Run("nested map", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "inner": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Binner%5D%5Bfoo%5D=bar", values.Encode()) + }) + + t.Run("nested map array", func(t *testing.T) { + type request struct { + Metadata map[string]interface{} `json:"metadata" url:"metadata"` + } + values, err := QueryValues( + &request{ + Metadata: map[string]interface{}{ + "inner": []string{ + "one", + "two", + "three", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "metadata%5Binner%5D=one&metadata%5Binner%5D=two&metadata%5Binner%5D=three", values.Encode()) + }) +} + +func TestQueryValuesWithDefaults(t *testing.T) { + t.Run("apply defaults to zero values", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + Enabled bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode()) + }) + + t.Run("preserve non-zero values over defaults", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + Enabled bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + values, err := QueryValuesWithDefaults(&example{ + Name: "actual-name", + Age: 30, + // Enabled remains false (zero value), should get default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "age=30&enabled=true&name=actual-name", values.Encode()) + }) + + t.Run("ignore defaults for fields not in struct", func(t *testing.T) { + type example struct { + Name string `json:"name" url:"name"` + Age int `json:"age" url:"age"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "nonexistent": "should-be-ignored", + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&name=default-name", values.Encode()) + }) + + t.Run("type conversion for compatible defaults", func(t *testing.T) { + type example struct { + Count int64 `json:"count" url:"count"` + Rate float64 `json:"rate" url:"rate"` + Message string `json:"message" url:"message"` + } + + defaults := map[string]interface{}{ + "count": int(100), // int -> int64 conversion + "rate": float32(2.5), // float32 -> float64 conversion + "message": "hello", // string -> string (no conversion needed) + } + + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "count=100&message=hello&rate=2.5", values.Encode()) + }) + + t.Run("mixed with pointer fields and omitempty", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + Optional *string `json:"optional,omitempty" url:"optional,omitempty"` + Count int `json:"count,omitempty" url:"count,omitempty"` + } + + defaultOptional := "default-optional" + defaults := map[string]interface{}{ + "required": "default-required", + "optional": &defaultOptional, // pointer type + "count": 42, + } + + values, err := QueryValuesWithDefaults(&example{ + Required: "custom-required", // should override default + // Optional is nil, should get default + // Count is 0, should get default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "count=42&optional=default-optional&required=custom-required", values.Encode()) + }) + + t.Run("override non-zero defaults with explicit zero values", func(t *testing.T) { + type example struct { + Name *string `json:"name" url:"name"` + Age *int `json:"age" url:"age"` + Enabled *bool `json:"enabled" url:"enabled"` + } + + defaults := map[string]interface{}{ + "name": "default-name", + "age": 25, + "enabled": true, + } + + // first, test that a properly empty request is overridden: + { + values, err := QueryValuesWithDefaults(&example{}, defaults) + require.NoError(t, err) + assert.Equal(t, "age=25&enabled=true&name=default-name", values.Encode()) + } + + // second, test that a request that contains zeros is not overridden: + var ( + name = "" + age = 0 + enabled = false + ) + values, err := QueryValuesWithDefaults(&example{ + Name: &name, // explicit empty string should override default + Age: &age, // explicit zero should override default + Enabled: &enabled, // explicit false should override default + }, defaults) + require.NoError(t, err) + assert.Equal(t, "age=0&enabled=false&name=", values.Encode()) + }) + + t.Run("nil input returns empty values", func(t *testing.T) { + defaults := map[string]any{ + "name": "default-name", + "age": 25, + } + + // Test with nil + values, err := QueryValuesWithDefaults(nil, defaults) + require.NoError(t, err) + assert.Empty(t, values) + + // Test with nil pointer + type example struct { + Name string `json:"name" url:"name"` + } + var nilPtr *example + values, err = QueryValuesWithDefaults(nilPtr, defaults) + require.NoError(t, err) + assert.Empty(t, values) + }) +} diff --git a/seed/go-sdk/basic-auth-optional/internal/retrier.go b/seed/go-sdk/basic-auth-optional/internal/retrier.go new file mode 100644 index 000000000000..02fd1fb7d3f1 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/retrier.go @@ -0,0 +1,239 @@ +package internal + +import ( + "crypto/rand" + "math/big" + "net/http" + "strconv" + "time" +) + +const ( + defaultRetryAttempts = 2 + minRetryDelay = 1000 * time.Millisecond + maxRetryDelay = 60000 * time.Millisecond +) + +// RetryOption adapts the behavior the *Retrier. +type RetryOption func(*retryOptions) + +// RetryFunc is a retryable HTTP function call (i.e. *http.Client.Do). +type RetryFunc func(*http.Request) (*http.Response, error) + +// WithMaxAttempts configures the maximum number of attempts +// of the *Retrier. +func WithMaxAttempts(attempts uint) RetryOption { + return func(opts *retryOptions) { + opts.attempts = attempts + } +} + +// Retrier retries failed requests a configurable number of times with an +// exponential back-off between each retry. +type Retrier struct { + attempts uint +} + +// NewRetrier constructs a new *Retrier with the given options, if any. +func NewRetrier(opts ...RetryOption) *Retrier { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + attempts := uint(defaultRetryAttempts) + if options.attempts > 0 { + attempts = options.attempts + } + return &Retrier{ + attempts: attempts, + } +} + +// Run issues the request and, upon failure, retries the request if possible. +// +// The request will be retried as long as the request is deemed retryable and the +// number of retry attempts has not grown larger than the configured retry limit. +func (r *Retrier) Run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + opts ...RetryOption, +) (*http.Response, error) { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + maxRetryAttempts := r.attempts + if options.attempts > 0 { + maxRetryAttempts = options.attempts + } + var ( + retryAttempt uint + previousError error + ) + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt, + previousError, + ) +} + +func (r *Retrier) run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + maxRetryAttempts uint, + retryAttempt uint, + previousError error, +) (*http.Response, error) { + if retryAttempt >= maxRetryAttempts { + return nil, previousError + } + + // If the call has been cancelled, don't issue the request. + if err := request.Context().Err(); err != nil { + return nil, err + } + + // Reset the request body for retries since the body may have already been read. + if retryAttempt > 0 && request.GetBody != nil { + requestBody, err := request.GetBody() + if err != nil { + return nil, err + } + request.Body = requestBody + } + + response, err := fn(request) + if err != nil { + return nil, err + } + + if r.shouldRetry(response) { + defer func() { _ = response.Body.Close() }() + + delay, err := r.retryDelay(response, retryAttempt) + if err != nil { + return nil, err + } + + time.Sleep(delay) + + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt+1, + decodeError(response, errorDecoder), + ) + } + + return response, nil +} + +// shouldRetry returns true if the request should be retried based on the given +// response status code. +func (r *Retrier) shouldRetry(response *http.Response) bool { + return response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusRequestTimeout || + response.StatusCode >= http.StatusInternalServerError +} + +// retryDelay calculates the delay time based on response headers, +// falling back to exponential backoff if no headers are present. +func (r *Retrier) retryDelay(response *http.Response, retryAttempt uint) (time.Duration, error) { + // Check for Retry-After header first (RFC 7231), applying no jitter + if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" { + // Parse as number of seconds... + if seconds, err := strconv.Atoi(retryAfter); err == nil { + delay := time.Duration(seconds) * time.Second + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return delay, nil + } + } + + // ...or as an HTTP date; both are valid + if retryTime, err := time.Parse(time.RFC1123, retryAfter); err == nil { + delay := time.Until(retryTime) + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return delay, nil + } + } + } + + // Then check for industry-standard X-RateLimit-Reset header, applying positive jitter + if rateLimitReset := response.Header.Get("X-RateLimit-Reset"); rateLimitReset != "" { + if resetTimestamp, err := strconv.ParseInt(rateLimitReset, 10, 64); err == nil { + // Assume Unix timestamp in seconds + resetTime := time.Unix(resetTimestamp, 0) + delay := time.Until(resetTime) + if delay > 0 { + if delay > maxRetryDelay { + delay = maxRetryDelay + } + return r.addPositiveJitter(delay) + } + } + } + + // Fall back to exponential backoff + return r.exponentialBackoff(retryAttempt) +} + +// exponentialBackoff calculates the delay time based on the retry attempt +// and applies symmetric jitter (±10% around the delay). +func (r *Retrier) exponentialBackoff(retryAttempt uint) (time.Duration, error) { + if retryAttempt > 63 { // 2^63+ would overflow uint64 + retryAttempt = 63 + } + + delay := minRetryDelay << retryAttempt + if delay > maxRetryDelay { + delay = maxRetryDelay + } + + return r.addSymmetricJitter(delay) +} + +// addJitterWithRange applies jitter to the given delay. +// minPercent and maxPercent define the jitter range (e.g., 100, 120 for +0% to +20%). +func (r *Retrier) addJitterWithRange(delay time.Duration, minPercent, maxPercent int) (time.Duration, error) { + jitterRange := big.NewInt(int64(delay * time.Duration(maxPercent-minPercent) / 100)) + jitter, err := rand.Int(rand.Reader, jitterRange) + if err != nil { + return 0, err + } + + jitteredDelay := delay + time.Duration(jitter.Int64()) + delay*time.Duration(minPercent-100)/100 + if jitteredDelay < minRetryDelay { + jitteredDelay = minRetryDelay + } + if jitteredDelay > maxRetryDelay { + jitteredDelay = maxRetryDelay + } + return jitteredDelay, nil +} + +// addPositiveJitter applies positive jitter to the given delay (100%-120% range). +func (r *Retrier) addPositiveJitter(delay time.Duration) (time.Duration, error) { + return r.addJitterWithRange(delay, 100, 120) +} + +// addSymmetricJitter applies symmetric jitter to the given delay (90%-110% range). +func (r *Retrier) addSymmetricJitter(delay time.Duration) (time.Duration, error) { + return r.addJitterWithRange(delay, 90, 110) +} + +type retryOptions struct { + attempts uint +} diff --git a/seed/go-sdk/basic-auth-optional/internal/retrier_test.go b/seed/go-sdk/basic-auth-optional/internal/retrier_test.go new file mode 100644 index 000000000000..c45822871638 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/retrier_test.go @@ -0,0 +1,352 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/basic-auth-optional/fern/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type RetryTestCase struct { + description string + + giveAttempts uint + giveStatusCodes []int + giveResponse *InternalTestResponse + + wantResponse *InternalTestResponse + wantError *core.APIError +} + +func TestRetrier(t *testing.T) { + tests := []*RetryTestCase{ + { + description: "retry request succeeds after multiple failures", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + giveResponse: &InternalTestResponse{ + Id: "1", + }, + wantResponse: &InternalTestResponse{ + Id: "1", + }, + }, + { + description: "retry request fails if MaxAttempts is exceeded", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusOK, + }, + wantError: &core.APIError{ + StatusCode: http.StatusRequestTimeout, + }, + }, + { + description: "retry durations increase exponentially and stay within the min and max delay values", + giveAttempts: 4, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + }, + { + description: "retry does not occur on status code 404", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusNotFound, http.StatusOK}, + wantError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + }, + { + description: "retries occur on status code 429", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK}, + }, + { + description: "retries occur on status code 408", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK}, + }, + { + description: "retries occur on status code 500", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK}, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + var ( + test = tc + server = newTestRetryServer(t, test) + client = server.Client() + ) + + t.Parallel() + + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodGet, + Request: &InternalTestRequest{}, + Response: &response, + MaxAttempts: test.giveAttempts, + ResponseIsOptional: true, + }, + ) + + if test.wantError != nil { + require.IsType(t, err, &core.APIError{}) + expectedErrorCode := test.wantError.StatusCode + actualErrorCode := err.(*core.APIError).StatusCode + assert.Equal(t, expectedErrorCode, actualErrorCode) + return + } + + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +// newTestRetryServer returns a new *httptest.Server configured with the +// given test parameters, suitable for testing retries. +func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server { + var index int + timestamps := make([]time.Time, 0, len(tc.giveStatusCodes)) + + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + timestamps = append(timestamps, time.Now()) + if index > 0 && index < len(expectedRetryDurations) { + // Ensure that the duration between retries increases exponentially, + // and that it is within the minimum and maximum retry delay values. + actualDuration := timestamps[index].Sub(timestamps[index-1]) + expectedDurationMin := expectedRetryDurations[index-1] * 50 / 100 + expectedDurationMax := expectedRetryDurations[index-1] * 150 / 100 + assert.True( + t, + actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax, + "expected duration to be in range [%v, %v], got %v", + expectedDurationMin, + expectedDurationMax, + actualDuration, + ) + assert.LessOrEqual( + t, + actualDuration, + maxRetryDelay, + "expected duration to be less than the maxRetryDelay (%v), got %v", + maxRetryDelay, + actualDuration, + ) + assert.GreaterOrEqual( + t, + actualDuration, + minRetryDelay, + "expected duration to be greater than the minRetryDelay (%v), got %v", + minRetryDelay, + actualDuration, + ) + } + + request := new(InternalTestRequest) + bytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + require.LessOrEqual(t, index, len(tc.giveStatusCodes)) + + statusCode := tc.giveStatusCodes[index] + + w.WriteHeader(statusCode) + + if tc.giveResponse != nil && statusCode == http.StatusOK { + bytes, err = json.Marshal(tc.giveResponse) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + } + + index++ + }, + ), + ) +} + +// expectedRetryDurations holds an array of calculated retry durations, +// where the index of the array should correspond to the retry attempt. +// +// Values are calculated based off of `minRetryDelay * 2^i`. +var expectedRetryDurations = []time.Duration{ + 1000 * time.Millisecond, // 500ms * 2^1 = 1000ms + 2000 * time.Millisecond, // 500ms * 2^2 = 2000ms + 4000 * time.Millisecond, // 500ms * 2^3 = 4000ms + 8000 * time.Millisecond, // 500ms * 2^4 = 8000ms +} + +func TestRetryWithRequestBody(t *testing.T) { + // This test verifies that POST requests with a body are properly retried. + // The request body should be re-sent on each retry attempt. + expectedBody := `{"id":"test-id"}` + var requestBodies []string + var requestCount int + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + bodyBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + requestBodies = append(requestBodies, string(bodyBytes)) + + if requestCount == 1 { + // First request - return retryable error + w.WriteHeader(http.StatusServiceUnavailable) + return + } + // Second request - return success + w.WriteHeader(http.StatusOK) + response := &InternalTestResponse{Id: "success"} + bytes, _ := json.Marshal(response) + _, _ = w.Write(bytes) + })) + defer server.Close() + + caller := NewCaller(&CallerParams{ + Client: server.Client(), + }) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodPost, + Request: &InternalTestRequest{Id: "test-id"}, + Response: &response, + MaxAttempts: 2, + ResponseIsOptional: true, + }, + ) + + require.NoError(t, err) + require.Equal(t, 2, requestCount, "Expected exactly 2 requests") + require.Len(t, requestBodies, 2, "Expected 2 request bodies to be captured") + + // Both requests should have the same non-empty body + assert.Equal(t, expectedBody, requestBodies[0], "First request body should match expected") + assert.Equal(t, expectedBody, requestBodies[1], "Second request body should match expected (retry should re-send body)") +} + +func TestRetryDelayTiming(t *testing.T) { + tests := []struct { + name string + headerName string + headerValueFunc func() string + expectedMinMs int64 + expectedMaxMs int64 + }{ + { + name: "retry-after with seconds value", + headerName: "retry-after", + headerValueFunc: func() string { + return "1" + }, + expectedMinMs: 500, + expectedMaxMs: 1500, + }, + { + name: "retry-after with HTTP date", + headerName: "retry-after", + headerValueFunc: func() string { + return time.Now().Add(3 * time.Second).Format(time.RFC1123) + }, + expectedMinMs: 1500, + expectedMaxMs: 4500, + }, + { + name: "x-ratelimit-reset with future timestamp", + headerName: "x-ratelimit-reset", + headerValueFunc: func() string { + return fmt.Sprintf("%d", time.Now().Add(3*time.Second).Unix()) + }, + expectedMinMs: 1500, + expectedMaxMs: 4500, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var timestamps []time.Time + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timestamps = append(timestamps, time.Now()) + if len(timestamps) == 1 { + // First request - return retryable error with header + w.Header().Set(tt.headerName, tt.headerValueFunc()) + w.WriteHeader(http.StatusTooManyRequests) + } else { + // Second request - return success + w.WriteHeader(http.StatusOK) + response := &InternalTestResponse{Id: "success"} + bytes, _ := json.Marshal(response) + _, _ = w.Write(bytes) + } + })) + defer server.Close() + + caller := NewCaller(&CallerParams{ + Client: server.Client(), + }) + + var response *InternalTestResponse + _, err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodGet, + Request: &InternalTestRequest{}, + Response: &response, + MaxAttempts: 2, + ResponseIsOptional: true, + }, + ) + + require.NoError(t, err) + require.Len(t, timestamps, 2, "Expected exactly 2 requests") + + actualDelayMs := timestamps[1].Sub(timestamps[0]).Milliseconds() + + assert.GreaterOrEqual(t, actualDelayMs, tt.expectedMinMs, + "Actual delay %dms should be >= expected min %dms", actualDelayMs, tt.expectedMinMs) + assert.LessOrEqual(t, actualDelayMs, tt.expectedMaxMs, + "Actual delay %dms should be <= expected max %dms", actualDelayMs, tt.expectedMaxMs) + }) + } +} diff --git a/seed/go-sdk/basic-auth-optional/internal/stringer.go b/seed/go-sdk/basic-auth-optional/internal/stringer.go new file mode 100644 index 000000000000..312801851e0e --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/stringer.go @@ -0,0 +1,13 @@ +package internal + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-sdk/basic-auth-optional/internal/time.go b/seed/go-sdk/basic-auth-optional/internal/time.go new file mode 100644 index 000000000000..57f901a35ed8 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/internal/time.go @@ -0,0 +1,165 @@ +package internal + +import ( + "encoding/json" + "fmt" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + // If the value is not a string, check if it is a number (unix epoch seconds). + var epoch int64 + if numErr := json.Unmarshal(data, &epoch); numErr == nil { + t := time.Unix(epoch, 0).UTC() + *d = DateTime{t: &t} + return nil + } + return err + } + + // Try RFC3339Nano first (superset of RFC3339, supports fractional seconds). + parsedTime, err := time.Parse(time.RFC3339Nano, raw) + if err == nil { + *d = DateTime{t: &parsedTime} + return nil + } + rfc3339NanoErr := err + + // Fall back to ISO 8601 without timezone (assume UTC). + parsedTime, err = time.Parse("2006-01-02T15:04:05", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + iso8601Err := err + + // Fall back to date-only format. + parsedTime, err = time.Parse("2006-01-02", raw) + if err == nil { + parsedTime = parsedTime.UTC() + *d = DateTime{t: &parsedTime} + return nil + } + dateOnlyErr := err + + return fmt.Errorf("unable to parse datetime string %q: tried RFC3339Nano (%v), ISO8601 (%v), date-only (%v)", raw, rfc3339NanoErr, iso8601Err, dateOnlyErr) +} diff --git a/seed/go-sdk/basic-auth-optional/option/request_option.go b/seed/go-sdk/basic-auth-optional/option/request_option.go new file mode 100644 index 000000000000..cf173ed05305 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/option/request_option.go @@ -0,0 +1,81 @@ +// Code generated by Fern. DO NOT EDIT. + +package option + +import ( + core "github.com/basic-auth-optional/fern/core" + http "net/http" + url "net/url" +) + +// RequestOption adapts the behavior of an individual request. +type RequestOption = core.RequestOption + +// WithBaseURL sets the base URL, overriding the default +// environment, if any. +func WithBaseURL(baseURL string) *core.BaseURLOption { + return &core.BaseURLOption{ + BaseURL: baseURL, + } +} + +// WithHTTPClient uses the given HTTPClient to issue the request. +func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption { + return &core.HTTPClientOption{ + HTTPClient: httpClient, + } +} + +// WithHTTPHeader adds the given http.Header to the request. +func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption { + return &core.HTTPHeaderOption{ + // Clone the headers so they can't be modified after the option call. + HTTPHeader: httpHeader.Clone(), + } +} + +// WithBodyProperties adds the given body properties to the request. +func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption { + copiedBodyProperties := make(map[string]interface{}, len(bodyProperties)) + for key, value := range bodyProperties { + copiedBodyProperties[key] = value + } + return &core.BodyPropertiesOption{ + BodyProperties: copiedBodyProperties, + } +} + +// WithQueryParameters adds the given query parameters to the request. +func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption { + copiedQueryParameters := make(url.Values, len(queryParameters)) + for key, values := range queryParameters { + copiedQueryParameters[key] = values + } + return &core.QueryParametersOption{ + QueryParameters: copiedQueryParameters, + } +} + +// WithMaxAttempts configures the maximum number of retry attempts. +func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { + return &core.MaxAttemptsOption{ + MaxAttempts: attempts, + } +} + +// WithMaxStreamBufSize configures the maximum buffer size for streaming responses. +// This controls the maximum size of a single message (in bytes) that the stream +// can process. By default, this is set to 1MB. +func WithMaxStreamBufSize(size int) *core.MaxBufSizeOption { + return &core.MaxBufSizeOption{ + MaxBufSize: size, + } +} + +// WithBasicAuth sets the 'Authorization: Basic ' request header. +func WithBasicAuth(username, password string) *core.BasicAuthOption { + return &core.BasicAuthOption{ + Username: username, + Password: password, + } +} diff --git a/seed/go-sdk/basic-auth-optional/pointer.go b/seed/go-sdk/basic-auth-optional/pointer.go new file mode 100644 index 000000000000..9be282560124 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/pointer.go @@ -0,0 +1,137 @@ +package basicauthoptional + +import ( + "time" + + "github.com/google/uuid" +) + +// Bool returns a pointer to the given bool value. +func Bool(b bool) *bool { + return &b +} + +// Byte returns a pointer to the given byte value. +func Byte(b byte) *byte { + return &b +} + +// Bytes returns a pointer to the given []byte value. +func Bytes(b []byte) *[]byte { + return &b +} + +// Complex64 returns a pointer to the given complex64 value. +func Complex64(c complex64) *complex64 { + return &c +} + +// Complex128 returns a pointer to the given complex128 value. +func Complex128(c complex128) *complex128 { + return &c +} + +// Float32 returns a pointer to the given float32 value. +func Float32(f float32) *float32 { + return &f +} + +// Float64 returns a pointer to the given float64 value. +func Float64(f float64) *float64 { + return &f +} + +// Int returns a pointer to the given int value. +func Int(i int) *int { + return &i +} + +// Int8 returns a pointer to the given int8 value. +func Int8(i int8) *int8 { + return &i +} + +// Int16 returns a pointer to the given int16 value. +func Int16(i int16) *int16 { + return &i +} + +// Int32 returns a pointer to the given int32 value. +func Int32(i int32) *int32 { + return &i +} + +// Int64 returns a pointer to the given int64 value. +func Int64(i int64) *int64 { + return &i +} + +// Rune returns a pointer to the given rune value. +func Rune(r rune) *rune { + return &r +} + +// String returns a pointer to the given string value. +func String(s string) *string { + return &s +} + +// Uint returns a pointer to the given uint value. +func Uint(u uint) *uint { + return &u +} + +// Uint8 returns a pointer to the given uint8 value. +func Uint8(u uint8) *uint8 { + return &u +} + +// Uint16 returns a pointer to the given uint16 value. +func Uint16(u uint16) *uint16 { + return &u +} + +// Uint32 returns a pointer to the given uint32 value. +func Uint32(u uint32) *uint32 { + return &u +} + +// Uint64 returns a pointer to the given uint64 value. +func Uint64(u uint64) *uint64 { + return &u +} + +// Uintptr returns a pointer to the given uintptr value. +func Uintptr(u uintptr) *uintptr { + return &u +} + +// UUID returns a pointer to the given uuid.UUID value. +func UUID(u uuid.UUID) *uuid.UUID { + return &u +} + +// Time returns a pointer to the given time.Time value. +func Time(t time.Time) *time.Time { + return &t +} + +// MustParseDate attempts to parse the given string as a +// date time.Time, and panics upon failure. +func MustParseDate(date string) time.Time { + t, err := time.Parse("2006-01-02", date) + if err != nil { + panic(err) + } + return t +} + +// MustParseDateTime attempts to parse the given string as a +// datetime time.Time, and panics upon failure. +func MustParseDateTime(datetime string) time.Time { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + panic(err) + } + return t +} diff --git a/seed/go-sdk/basic-auth-optional/pointer_test.go b/seed/go-sdk/basic-auth-optional/pointer_test.go new file mode 100644 index 000000000000..3769e25c530a --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/pointer_test.go @@ -0,0 +1,211 @@ +package basicauthoptional + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestBool(t *testing.T) { + value := true + ptr := Bool(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestByte(t *testing.T) { + value := byte(42) + ptr := Byte(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestComplex64(t *testing.T) { + value := complex64(1 + 2i) + ptr := Complex64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestComplex128(t *testing.T) { + value := complex128(1 + 2i) + ptr := Complex128(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestFloat32(t *testing.T) { + value := float32(3.14) + ptr := Float32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestFloat64(t *testing.T) { + value := 3.14159 + ptr := Float64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt(t *testing.T) { + value := 42 + ptr := Int(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt8(t *testing.T) { + value := int8(42) + ptr := Int8(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt16(t *testing.T) { + value := int16(42) + ptr := Int16(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt32(t *testing.T) { + value := int32(42) + ptr := Int32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestInt64(t *testing.T) { + value := int64(42) + ptr := Int64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestRune(t *testing.T) { + value := 'A' + ptr := Rune(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestString(t *testing.T) { + value := "hello" + ptr := String(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint(t *testing.T) { + value := uint(42) + ptr := Uint(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint8(t *testing.T) { + value := uint8(42) + ptr := Uint8(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint16(t *testing.T) { + value := uint16(42) + ptr := Uint16(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint32(t *testing.T) { + value := uint32(42) + ptr := Uint32(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUint64(t *testing.T) { + value := uint64(42) + ptr := Uint64(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUintptr(t *testing.T) { + value := uintptr(42) + ptr := Uintptr(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestUUID(t *testing.T) { + value := uuid.New() + ptr := UUID(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestTime(t *testing.T) { + value := time.Now() + ptr := Time(value) + assert.NotNil(t, ptr) + assert.Equal(t, value, *ptr) +} + +func TestMustParseDate(t *testing.T) { + t.Run("valid date", func(t *testing.T) { + result := MustParseDate("2024-01-15") + expected, _ := time.Parse("2006-01-02", "2024-01-15") + assert.Equal(t, expected, result) + }) + + t.Run("invalid date panics", func(t *testing.T) { + assert.Panics(t, func() { + MustParseDate("invalid-date") + }) + }) +} + +func TestMustParseDateTime(t *testing.T) { + t.Run("valid datetime", func(t *testing.T) { + result := MustParseDateTime("2024-01-15T10:30:00Z") + expected, _ := time.Parse(time.RFC3339, "2024-01-15T10:30:00Z") + assert.Equal(t, expected, result) + }) + + t.Run("invalid datetime panics", func(t *testing.T) { + assert.Panics(t, func() { + MustParseDateTime("invalid-datetime") + }) + }) +} + +func TestPointerHelpersWithZeroValues(t *testing.T) { + t.Run("zero bool", func(t *testing.T) { + ptr := Bool(false) + assert.NotNil(t, ptr) + assert.Equal(t, false, *ptr) + }) + + t.Run("zero int", func(t *testing.T) { + ptr := Int(0) + assert.NotNil(t, ptr) + assert.Equal(t, 0, *ptr) + }) + + t.Run("empty string", func(t *testing.T) { + ptr := String("") + assert.NotNil(t, ptr) + assert.Equal(t, "", *ptr) + }) + + t.Run("zero time", func(t *testing.T) { + zeroTime := time.Time{} + ptr := Time(zeroTime) + assert.NotNil(t, ptr) + assert.Equal(t, zeroTime, *ptr) + }) +} diff --git a/seed/go-sdk/basic-auth-optional/reference.md b/seed/go-sdk/basic-auth-optional/reference.md new file mode 100644 index 000000000000..d7481978d71f --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/reference.md @@ -0,0 +1,105 @@ +# Reference +## BasicAuth +
client.BasicAuth.GetWithBasicAuth() -> bool +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +client.BasicAuth.GetWithBasicAuth( + context.TODO(), + ) +} +``` +
+
+
+
+ + +
+
+
+ +
client.BasicAuth.PostWithBasicAuth(request) -> bool +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```go +request := map[string]any{ + "key": "value", + } +client.BasicAuth.PostWithBasicAuth( + context.TODO(), + request, + ) +} +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `any` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/go-sdk/basic-auth-optional/snippet.json b/seed/go-sdk/basic-auth-optional/snippet.json new file mode 100644 index 000000000000..b5e8b81b2d5f --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/snippet.json @@ -0,0 +1,26 @@ +{ + "endpoints": [ + { + "id": { + "path": "/basic-auth", + "method": "GET", + "identifier_override": "endpoint_basic-auth.getWithBasicAuth" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-optional/fern/client\"\n\toption \"github.com/basic-auth-optional/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/basic-auth", + "method": "POST", + "identifier_override": "endpoint_basic-auth.postWithBasicAuth" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-optional/fern/client\"\n\toption \"github.com/basic-auth-optional/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/go-sdk/basic-auth-optional/types.go b/seed/go-sdk/basic-auth-optional/types.go new file mode 100644 index 000000000000..fc9f46088535 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/types.go @@ -0,0 +1,94 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauthoptional + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/basic-auth-optional/fern/internal" + big "math/big" +) + +var ( + unauthorizedRequestErrorBodyFieldMessage = big.NewInt(1 << 0) +) + +type UnauthorizedRequestErrorBody struct { + Message string `json:"message" url:"message"` + + // Private bitmask of fields set to an explicit value and therefore not to be omitted + explicitFields *big.Int `json:"-" url:"-"` + + extraProperties map[string]interface{} + rawJSON json.RawMessage +} + +func (u *UnauthorizedRequestErrorBody) GetMessage() string { + if u == nil { + return "" + } + return u.Message +} + +func (u *UnauthorizedRequestErrorBody) GetExtraProperties() map[string]interface{} { + if u == nil { + return nil + } + return u.extraProperties +} + +func (u *UnauthorizedRequestErrorBody) require(field *big.Int) { + if u.explicitFields == nil { + u.explicitFields = big.NewInt(0) + } + u.explicitFields.Or(u.explicitFields, field) +} + +// SetMessage sets the Message field and marks it as non-optional; +// this prevents an empty or null value for this field from being omitted during serialization. +func (u *UnauthorizedRequestErrorBody) SetMessage(message string) { + u.Message = message + u.require(unauthorizedRequestErrorBodyFieldMessage) +} + +func (u *UnauthorizedRequestErrorBody) UnmarshalJSON(data []byte) error { + type unmarshaler UnauthorizedRequestErrorBody + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *u = UnauthorizedRequestErrorBody(value) + extraProperties, err := internal.ExtractExtraProperties(data, *u) + if err != nil { + return err + } + u.extraProperties = extraProperties + u.rawJSON = json.RawMessage(data) + return nil +} + +func (u *UnauthorizedRequestErrorBody) MarshalJSON() ([]byte, error) { + type embed UnauthorizedRequestErrorBody + var marshaler = struct { + embed + }{ + embed: embed(*u), + } + explicitMarshaler := internal.HandleExplicitFields(marshaler, u.explicitFields) + return json.Marshal(explicitMarshaler) +} + +func (u *UnauthorizedRequestErrorBody) String() string { + if u == nil { + return "" + } + if len(u.rawJSON) > 0 { + if value, err := internal.StringifyJSON(u.rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(u); err == nil { + return value + } + return fmt.Sprintf("%#v", u) +} diff --git a/seed/go-sdk/basic-auth-optional/types_test.go b/seed/go-sdk/basic-auth-optional/types_test.go new file mode 100644 index 000000000000..b02b34054488 --- /dev/null +++ b/seed/go-sdk/basic-auth-optional/types_test.go @@ -0,0 +1,153 @@ +// Code generated by Fern. DO NOT EDIT. + +package basicauthoptional + +import ( + json "encoding/json" + assert "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" + testing "testing" +) + +func TestSettersUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("SetMessage", func(t *testing.T) { + obj := &UnauthorizedRequestErrorBody{} + var fernTestValueMessage string + obj.SetMessage(fernTestValueMessage) + assert.Equal(t, fernTestValueMessage, obj.Message) + assert.NotNil(t, obj.explicitFields) + }) + +} + +func TestGettersUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("GetMessage", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &UnauthorizedRequestErrorBody{} + var expected string + obj.Message = expected + + // Act & Assert + assert.Equal(t, expected, obj.GetMessage(), "getter should return the property value") + }) + + t.Run("GetMessage_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *UnauthorizedRequestErrorBody + // Should not panic - getters should handle nil receiver gracefully + defer func() { + if r := recover(); r != nil { + t.Errorf("Getter panicked on nil receiver: %v", r) + } + }() + _ = obj.GetMessage() // Should return zero value + }) + +} + +func TestSettersMarkExplicitUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("SetMessage_MarksExplicit", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &UnauthorizedRequestErrorBody{} + var fernTestValueMessage string + + // Act + obj.SetMessage(fernTestValueMessage) + + // Assert - object with explicitly set field can be marshaled/unmarshaled + bytes, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed for test setup") + + // This test ensures JSON marshaling and unmarshaling succeed when the field has a zero/nil value + // Detect if marshaled JSON is an object or primitive to use correct unmarshal target + if len(bytes) > 0 && bytes[0] == '{' { + // JSON object - unmarshal into map + var unmarshaled map[string]interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } else { + // JSON primitive (string, number, boolean, null) - unmarshal into interface{} + var unmarshaled interface{} + err = json.Unmarshal(bytes, &unmarshaled) + require.NoError(t, err, "unmarshaling should succeed for test verification") + } + + // Note: This does not explicitly assert the presence of a specific JSON field + // It verifies that setting a field via setter allows successful JSON round-trip + }) + +} + +func TestJSONMarshalingUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("MarshalUnmarshal", func(t *testing.T) { + t.Parallel() + // Arrange + obj := &UnauthorizedRequestErrorBody{} + + // Act - Marshal to JSON + data, err := json.Marshal(obj) + require.NoError(t, err, "marshaling should succeed") + assert.NotNil(t, data, "marshaled data should not be nil") + assert.NotEmpty(t, data, "marshaled data should not be empty") + + // Unmarshal back and verify round-trip + var unmarshaled UnauthorizedRequestErrorBody + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err, "round-trip unmarshal should succeed") + }) + + t.Run("UnmarshalInvalidJSON", func(t *testing.T) { + t.Parallel() + var obj UnauthorizedRequestErrorBody + err := json.Unmarshal([]byte(`{invalid json}`), &obj) + assert.Error(t, err, "unmarshaling invalid JSON should return an error") + }) + + t.Run("UnmarshalEmptyObject", func(t *testing.T) { + t.Parallel() + var obj UnauthorizedRequestErrorBody + err := json.Unmarshal([]byte(`{}`), &obj) + assert.NoError(t, err, "unmarshaling empty object should succeed") + }) +} + +func TestStringUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("StringMethod", func(t *testing.T) { + t.Parallel() + obj := &UnauthorizedRequestErrorBody{} + result := obj.String() + assert.NotEmpty(t, result, "String() should return a non-empty representation") + }) + + t.Run("StringMethod_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *UnauthorizedRequestErrorBody + result := obj.String() + assert.Equal(t, "", result, "String() should return for nil receiver") + }) +} + +func TestExtraPropertiesUnauthorizedRequestErrorBody(t *testing.T) { + t.Run("GetExtraProperties", func(t *testing.T) { + t.Parallel() + obj := &UnauthorizedRequestErrorBody{} + // Should not panic when calling GetExtraProperties() + defer func() { + if r := recover(); r != nil { + t.Errorf("GetExtraProperties() panicked: %v", r) + } + }() + extraProps := obj.GetExtraProperties() + // Result can be nil or an empty/non-empty map + _ = extraProps + }) + + t.Run("GetExtraProperties_NilReceiver", func(t *testing.T) { + t.Parallel() + var obj *UnauthorizedRequestErrorBody + extraProps := obj.GetExtraProperties() + assert.Nil(t, extraProps, "nil receiver should return nil without panicking") + }) +} diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml new file mode 100644 index 000000000000..8b1d72b0b769 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml @@ -0,0 +1,12 @@ +name: basic-auth-optional +auth: Basic +auth-schemes: + Basic: + scheme: basic + username: + name: username + password: + name: password + omit: true +error-discrimination: + strategy: status-code diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml new file mode 100644 index 000000000000..6a21fd56c9f9 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + errors: ./errors.yml + +service: + auth: false + base-path: "" + endpoints: + getWithBasicAuth: + auth: true + docs: GET request with basic auth scheme + path: /basic-auth + method: GET + response: + boolean + examples: + - response: + body: true + errors: + - errors.UnauthorizedRequest + + postWithBasicAuth: + auth: true + docs: POST request with basic auth scheme + path: /basic-auth + method: POST + request: + name: PostWithBasicAuth + body: unknown + response: boolean + examples: + - request: + key: "value" + response: + body: true + errors: + - errors.UnauthorizedRequest + - errors.BadRequest diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml new file mode 100644 index 000000000000..cdd6a9667031 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml @@ -0,0 +1,11 @@ +errors: + UnauthorizedRequest: + status-code: 401 + type: UnauthorizedRequestErrorBody + BadRequest: + status-code: 400 + +types: + UnauthorizedRequestErrorBody: + properties: + message: string diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-optional/generators.yml new file mode 100644 index 000000000000..b30d6ed97cd2 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/generators.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: + php-sdk: + generators: + - name: fernapi/fern-php-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/php-sdk-tests + branch: basic-auth-optional + go-sdk: + generators: + - name: fernapi/fern-go-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/go-sdk-tests + branch: basic-auth-optional From 13cbed4e57bfa4b1537c6895fa220ed13f23367d Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:38:57 +0000 Subject: [PATCH 02/12] refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...e_errors_UnauthorizedRequestErrorBody.json | 0 seed/go-sdk/basic-auth-optional/snippet.json | 26 ------------------- .../.fern/metadata.json | 0 .../.github/workflows/ci.yml | 0 .../README.md | 4 +-- .../basicauth/client.go | 6 ++--- .../basicauth/raw_client.go | 8 +++--- .../client/client.go | 8 +++--- .../client/client_test.go | 2 +- .../core/api_error.go | 0 .../core/http.go | 0 .../core/request_option.go | 4 +-- .../dynamic-snippets/example0}/snippet.go | 4 +-- .../dynamic-snippets/example1}/snippet.go | 4 +-- .../dynamic-snippets/example2}/snippet.go | 4 +-- .../dynamic-snippets/example3}/snippet.go | 4 +-- .../dynamic-snippets/example4}/snippet.go | 4 +-- .../dynamic-snippets/example5/snippet.go | 4 +-- .../dynamic-snippets/example6/snippet.go | 4 +-- .../error_codes.go | 4 +-- .../errors.go | 2 +- .../file_param.go | 0 .../go.mod | 2 +- .../go.sum | 0 .../internal/caller.go | 2 +- .../internal/caller_test.go | 2 +- .../internal/error_decoder.go | 2 +- .../internal/error_decoder_test.go | 2 +- .../internal/explicit_fields.go | 0 .../internal/explicit_fields_test.go | 0 .../internal/extra_properties.go | 0 .../internal/extra_properties_test.go | 0 .../internal/http.go | 0 .../internal/query.go | 0 .../internal/query_test.go | 0 .../internal/retrier.go | 0 .../internal/retrier_test.go | 2 +- .../internal/stringer.go | 0 .../internal/time.go | 0 .../option/request_option.go | 2 +- .../pointer.go | 0 .../pointer_test.go | 0 .../reference.md | 0 .../go-sdk/basic-auth-pw-omitted/snippet.json | 26 +++++++++++++++++++ .../types.go | 2 +- .../types_test.go | 0 .../definition/api.yml | 2 +- .../definition/basic-auth.yml | 0 .../definition/errors.yml | 0 .../generators.yml | 4 +-- 50 files changed, 70 insertions(+), 70 deletions(-) rename packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/{basic-auth-optional => basic-auth-pw-omitted}/type_errors_UnauthorizedRequestErrorBody.json (100%) delete mode 100644 seed/go-sdk/basic-auth-optional/snippet.json rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.fern/metadata.json (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.github/workflows/ci.yml (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/README.md (98%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/basicauth/client.go (87%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/basicauth/raw_client.go (92%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/client/client.go (71%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/client/client_test.go (94%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/core/api_error.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/core/http.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/core/request_option.go (96%) rename seed/go-sdk/{basic-auth-optional/dynamic-snippets/example1 => basic-auth-pw-omitted/dynamic-snippets/example0}/snippet.go (74%) rename seed/go-sdk/{basic-auth-optional/dynamic-snippets/example2 => basic-auth-pw-omitted/dynamic-snippets/example1}/snippet.go (74%) rename seed/go-sdk/{basic-auth-optional/dynamic-snippets/example0 => basic-auth-pw-omitted/dynamic-snippets/example2}/snippet.go (74%) rename seed/go-sdk/{basic-auth-optional/dynamic-snippets/example4 => basic-auth-pw-omitted/dynamic-snippets/example3}/snippet.go (78%) rename seed/go-sdk/{basic-auth-optional/dynamic-snippets/example3 => basic-auth-pw-omitted/dynamic-snippets/example4}/snippet.go (78%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example5/snippet.go (78%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example6/snippet.go (78%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/error_codes.go (75%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/errors.go (93%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/file_param.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/go.mod (85%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/go.sum (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/caller.go (99%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/caller_test.go (99%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/error_decoder.go (97%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/error_decoder_test.go (97%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/explicit_fields.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/explicit_fields_test.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/extra_properties.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/extra_properties_test.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/http.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/query.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/query_test.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/retrier.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/retrier_test.go (99%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/stringer.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/internal/time.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/option/request_option.go (97%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/pointer.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/pointer_test.go (100%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/reference.md (100%) create mode 100644 seed/go-sdk/basic-auth-pw-omitted/snippet.json rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/types.go (97%) rename seed/go-sdk/{basic-auth-optional => basic-auth-pw-omitted}/types_test.go (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/api.yml (86%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/basic-auth.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/errors.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/generators.yml (86%) diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json similarity index 100% rename from packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json rename to packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json diff --git a/seed/go-sdk/basic-auth-optional/snippet.json b/seed/go-sdk/basic-auth-optional/snippet.json deleted file mode 100644 index b5e8b81b2d5f..000000000000 --- a/seed/go-sdk/basic-auth-optional/snippet.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "endpoints": [ - { - "id": { - "path": "/basic-auth", - "method": "GET", - "identifier_override": "endpoint_basic-auth.getWithBasicAuth" - }, - "snippet": { - "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-optional/fern/client\"\n\toption \"github.com/basic-auth-optional/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" - } - }, - { - "id": { - "path": "/basic-auth", - "method": "POST", - "identifier_override": "endpoint_basic-auth.postWithBasicAuth" - }, - "snippet": { - "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-optional/fern/client\"\n\toption \"github.com/basic-auth-optional/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" - } - } - ] -} \ No newline at end of file diff --git a/seed/go-sdk/basic-auth-optional/.fern/metadata.json b/seed/go-sdk/basic-auth-pw-omitted/.fern/metadata.json similarity index 100% rename from seed/go-sdk/basic-auth-optional/.fern/metadata.json rename to seed/go-sdk/basic-auth-pw-omitted/.fern/metadata.json diff --git a/seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/go-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml similarity index 100% rename from seed/go-sdk/basic-auth-optional/.github/workflows/ci.yml rename to seed/go-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml diff --git a/seed/go-sdk/basic-auth-optional/README.md b/seed/go-sdk/basic-auth-pw-omitted/README.md similarity index 98% rename from seed/go-sdk/basic-auth-optional/README.md rename to seed/go-sdk/basic-auth-pw-omitted/README.md index 38f9aa12a7b8..eea818e81fbe 100644 --- a/seed/go-sdk/basic-auth-optional/README.md +++ b/seed/go-sdk/basic-auth-pw-omitted/README.md @@ -32,8 +32,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/basicauth/client.go b/seed/go-sdk/basic-auth-pw-omitted/basicauth/client.go similarity index 87% rename from seed/go-sdk/basic-auth-optional/basicauth/client.go rename to seed/go-sdk/basic-auth-pw-omitted/basicauth/client.go index f31be0b83313..157d58f95786 100644 --- a/seed/go-sdk/basic-auth-optional/basicauth/client.go +++ b/seed/go-sdk/basic-auth-pw-omitted/basicauth/client.go @@ -5,9 +5,9 @@ package basicauth import ( context "context" - core "github.com/basic-auth-optional/fern/core" - internal "github.com/basic-auth-optional/fern/internal" - option "github.com/basic-auth-optional/fern/option" + core "github.com/basic-auth-pw-omitted/fern/core" + internal "github.com/basic-auth-pw-omitted/fern/internal" + option "github.com/basic-auth-pw-omitted/fern/option" ) type Client struct { diff --git a/seed/go-sdk/basic-auth-optional/basicauth/raw_client.go b/seed/go-sdk/basic-auth-pw-omitted/basicauth/raw_client.go similarity index 92% rename from seed/go-sdk/basic-auth-optional/basicauth/raw_client.go rename to seed/go-sdk/basic-auth-pw-omitted/basicauth/raw_client.go index b4aae98cd3e8..d91deb21d31b 100644 --- a/seed/go-sdk/basic-auth-optional/basicauth/raw_client.go +++ b/seed/go-sdk/basic-auth-pw-omitted/basicauth/raw_client.go @@ -6,10 +6,10 @@ import ( context "context" http "net/http" - fern "github.com/basic-auth-optional/fern" - core "github.com/basic-auth-optional/fern/core" - internal "github.com/basic-auth-optional/fern/internal" - option "github.com/basic-auth-optional/fern/option" + fern "github.com/basic-auth-pw-omitted/fern" + core "github.com/basic-auth-pw-omitted/fern/core" + internal "github.com/basic-auth-pw-omitted/fern/internal" + option "github.com/basic-auth-pw-omitted/fern/option" ) type RawClient struct { diff --git a/seed/go-sdk/basic-auth-optional/client/client.go b/seed/go-sdk/basic-auth-pw-omitted/client/client.go similarity index 71% rename from seed/go-sdk/basic-auth-optional/client/client.go rename to seed/go-sdk/basic-auth-pw-omitted/client/client.go index ad934300ce9a..921538369fec 100644 --- a/seed/go-sdk/basic-auth-optional/client/client.go +++ b/seed/go-sdk/basic-auth-pw-omitted/client/client.go @@ -3,10 +3,10 @@ package client import ( - basicauth "github.com/basic-auth-optional/fern/basicauth" - core "github.com/basic-auth-optional/fern/core" - internal "github.com/basic-auth-optional/fern/internal" - option "github.com/basic-auth-optional/fern/option" + basicauth "github.com/basic-auth-pw-omitted/fern/basicauth" + core "github.com/basic-auth-pw-omitted/fern/core" + internal "github.com/basic-auth-pw-omitted/fern/internal" + option "github.com/basic-auth-pw-omitted/fern/option" ) type Client struct { diff --git a/seed/go-sdk/basic-auth-optional/client/client_test.go b/seed/go-sdk/basic-auth-pw-omitted/client/client_test.go similarity index 94% rename from seed/go-sdk/basic-auth-optional/client/client_test.go rename to seed/go-sdk/basic-auth-pw-omitted/client/client_test.go index a67e052e5add..4c8e41d51930 100644 --- a/seed/go-sdk/basic-auth-optional/client/client_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/client/client_test.go @@ -3,7 +3,7 @@ package client import ( - option "github.com/basic-auth-optional/fern/option" + option "github.com/basic-auth-pw-omitted/fern/option" assert "github.com/stretchr/testify/assert" http "net/http" testing "testing" diff --git a/seed/go-sdk/basic-auth-optional/core/api_error.go b/seed/go-sdk/basic-auth-pw-omitted/core/api_error.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/core/api_error.go rename to seed/go-sdk/basic-auth-pw-omitted/core/api_error.go diff --git a/seed/go-sdk/basic-auth-optional/core/http.go b/seed/go-sdk/basic-auth-pw-omitted/core/http.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/core/http.go rename to seed/go-sdk/basic-auth-pw-omitted/core/http.go diff --git a/seed/go-sdk/basic-auth-optional/core/request_option.go b/seed/go-sdk/basic-auth-pw-omitted/core/request_option.go similarity index 96% rename from seed/go-sdk/basic-auth-optional/core/request_option.go rename to seed/go-sdk/basic-auth-pw-omitted/core/request_option.go index 0b0f3c2155c2..93d7f0a3824a 100644 --- a/seed/go-sdk/basic-auth-optional/core/request_option.go +++ b/seed/go-sdk/basic-auth-pw-omitted/core/request_option.go @@ -58,9 +58,9 @@ func (r *RequestOptions) ToHeader() http.Header { func (r *RequestOptions) cloneHeader() http.Header { headers := r.HTTPHeader.Clone() headers.Set("X-Fern-Language", "Go") - headers.Set("X-Fern-SDK-Name", "github.com/basic-auth-optional/fern") + headers.Set("X-Fern-SDK-Name", "github.com/basic-auth-pw-omitted/fern") headers.Set("X-Fern-SDK-Version", "v0.0.1") - headers.Set("User-Agent", "github.com/basic-auth-optional/fern/0.0.1") + headers.Set("User-Agent", "github.com/basic-auth-pw-omitted/fern/0.0.1") return headers } diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.go similarity index 74% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.go index 3cf23a6c80a0..f2a3bdab304b 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example1/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.go similarity index 74% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.go index 3cf23a6c80a0..f2a3bdab304b 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example2/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.go similarity index 74% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.go index 3cf23a6c80a0..f2a3bdab304b 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example0/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.go similarity index 78% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.go index e873a933abb8..299e22b0c3cd 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example4/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.go similarity index 78% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.go index e873a933abb8..299e22b0c3cd 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example3/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.go similarity index 78% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.go index e873a933abb8..299e22b0c3cd 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example5/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.go similarity index 78% rename from seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go rename to seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.go index e873a933abb8..299e22b0c3cd 100644 --- a/seed/go-sdk/basic-auth-optional/dynamic-snippets/example6/snippet.go +++ b/seed/go-sdk/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.go @@ -3,8 +3,8 @@ package example import ( context "context" - client "github.com/basic-auth-optional/fern/client" - option "github.com/basic-auth-optional/fern/option" + client "github.com/basic-auth-pw-omitted/fern/client" + option "github.com/basic-auth-pw-omitted/fern/option" ) func do() { diff --git a/seed/go-sdk/basic-auth-optional/error_codes.go b/seed/go-sdk/basic-auth-pw-omitted/error_codes.go similarity index 75% rename from seed/go-sdk/basic-auth-optional/error_codes.go rename to seed/go-sdk/basic-auth-pw-omitted/error_codes.go index a3e89f4ec571..62042f8cd6dc 100644 --- a/seed/go-sdk/basic-auth-optional/error_codes.go +++ b/seed/go-sdk/basic-auth-pw-omitted/error_codes.go @@ -3,8 +3,8 @@ package basicauthoptional import ( - core "github.com/basic-auth-optional/fern/core" - internal "github.com/basic-auth-optional/fern/internal" + core "github.com/basic-auth-pw-omitted/fern/core" + internal "github.com/basic-auth-pw-omitted/fern/internal" ) var ErrorCodes internal.ErrorCodes = internal.ErrorCodes{ diff --git a/seed/go-sdk/basic-auth-optional/errors.go b/seed/go-sdk/basic-auth-pw-omitted/errors.go similarity index 93% rename from seed/go-sdk/basic-auth-optional/errors.go rename to seed/go-sdk/basic-auth-pw-omitted/errors.go index cf0fe91ad9fe..b85af2ecd351 100644 --- a/seed/go-sdk/basic-auth-optional/errors.go +++ b/seed/go-sdk/basic-auth-pw-omitted/errors.go @@ -4,7 +4,7 @@ package basicauthoptional import ( json "encoding/json" - core "github.com/basic-auth-optional/fern/core" + core "github.com/basic-auth-pw-omitted/fern/core" ) type BadRequest struct { diff --git a/seed/go-sdk/basic-auth-optional/file_param.go b/seed/go-sdk/basic-auth-pw-omitted/file_param.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/file_param.go rename to seed/go-sdk/basic-auth-pw-omitted/file_param.go diff --git a/seed/go-sdk/basic-auth-optional/go.mod b/seed/go-sdk/basic-auth-pw-omitted/go.mod similarity index 85% rename from seed/go-sdk/basic-auth-optional/go.mod rename to seed/go-sdk/basic-auth-pw-omitted/go.mod index 8c580a08cdd9..373707c14f77 100644 --- a/seed/go-sdk/basic-auth-optional/go.mod +++ b/seed/go-sdk/basic-auth-pw-omitted/go.mod @@ -1,4 +1,4 @@ -module github.com/basic-auth-optional/fern +module github.com/basic-auth-pw-omitted/fern go 1.21 diff --git a/seed/go-sdk/basic-auth-optional/go.sum b/seed/go-sdk/basic-auth-pw-omitted/go.sum similarity index 100% rename from seed/go-sdk/basic-auth-optional/go.sum rename to seed/go-sdk/basic-auth-pw-omitted/go.sum diff --git a/seed/go-sdk/basic-auth-optional/internal/caller.go b/seed/go-sdk/basic-auth-pw-omitted/internal/caller.go similarity index 99% rename from seed/go-sdk/basic-auth-optional/internal/caller.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/caller.go index 665f4ecc2f50..1792ba15aa25 100644 --- a/seed/go-sdk/basic-auth-optional/internal/caller.go +++ b/seed/go-sdk/basic-auth-pw-omitted/internal/caller.go @@ -12,7 +12,7 @@ import ( "reflect" "strings" - "github.com/basic-auth-optional/fern/core" + "github.com/basic-auth-pw-omitted/fern/core" ) const ( diff --git a/seed/go-sdk/basic-auth-optional/internal/caller_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/caller_test.go similarity index 99% rename from seed/go-sdk/basic-auth-optional/internal/caller_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/caller_test.go index 50b1ea8969d5..2b36796c6c34 100644 --- a/seed/go-sdk/basic-auth-optional/internal/caller_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/internal/caller_test.go @@ -14,7 +14,7 @@ import ( "strings" "testing" - "github.com/basic-auth-optional/fern/core" + "github.com/basic-auth-pw-omitted/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/seed/go-sdk/basic-auth-optional/internal/error_decoder.go b/seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder.go similarity index 97% rename from seed/go-sdk/basic-auth-optional/internal/error_decoder.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder.go index ebafe345fa1e..e42cce0488d5 100644 --- a/seed/go-sdk/basic-auth-optional/internal/error_decoder.go +++ b/seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder.go @@ -8,7 +8,7 @@ import ( "io" "net/http" - "github.com/basic-auth-optional/fern/core" + "github.com/basic-auth-pw-omitted/fern/core" ) // ErrorCodes maps HTTP status codes to error constructors. diff --git a/seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder_test.go similarity index 97% rename from seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder_test.go index 85b529f99ec1..f3458b65a4d5 100644 --- a/seed/go-sdk/basic-auth-optional/internal/error_decoder_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/internal/error_decoder_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/basic-auth-optional/fern/core" + "github.com/basic-auth-pw-omitted/fern/core" "github.com/stretchr/testify/assert" ) diff --git a/seed/go-sdk/basic-auth-optional/internal/explicit_fields.go b/seed/go-sdk/basic-auth-pw-omitted/internal/explicit_fields.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/explicit_fields.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/explicit_fields.go diff --git a/seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/explicit_fields_test.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/explicit_fields_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/explicit_fields_test.go diff --git a/seed/go-sdk/basic-auth-optional/internal/extra_properties.go b/seed/go-sdk/basic-auth-pw-omitted/internal/extra_properties.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/extra_properties.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/extra_properties.go diff --git a/seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/extra_properties_test.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/extra_properties_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/extra_properties_test.go diff --git a/seed/go-sdk/basic-auth-optional/internal/http.go b/seed/go-sdk/basic-auth-pw-omitted/internal/http.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/http.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/http.go diff --git a/seed/go-sdk/basic-auth-optional/internal/query.go b/seed/go-sdk/basic-auth-pw-omitted/internal/query.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/query.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/query.go diff --git a/seed/go-sdk/basic-auth-optional/internal/query_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/query_test.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/query_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/query_test.go diff --git a/seed/go-sdk/basic-auth-optional/internal/retrier.go b/seed/go-sdk/basic-auth-pw-omitted/internal/retrier.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/retrier.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/retrier.go diff --git a/seed/go-sdk/basic-auth-optional/internal/retrier_test.go b/seed/go-sdk/basic-auth-pw-omitted/internal/retrier_test.go similarity index 99% rename from seed/go-sdk/basic-auth-optional/internal/retrier_test.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/retrier_test.go index c45822871638..8fc4de3ebcce 100644 --- a/seed/go-sdk/basic-auth-optional/internal/retrier_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/internal/retrier_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/basic-auth-optional/fern/core" + "github.com/basic-auth-pw-omitted/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/seed/go-sdk/basic-auth-optional/internal/stringer.go b/seed/go-sdk/basic-auth-pw-omitted/internal/stringer.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/stringer.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/stringer.go diff --git a/seed/go-sdk/basic-auth-optional/internal/time.go b/seed/go-sdk/basic-auth-pw-omitted/internal/time.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/internal/time.go rename to seed/go-sdk/basic-auth-pw-omitted/internal/time.go diff --git a/seed/go-sdk/basic-auth-optional/option/request_option.go b/seed/go-sdk/basic-auth-pw-omitted/option/request_option.go similarity index 97% rename from seed/go-sdk/basic-auth-optional/option/request_option.go rename to seed/go-sdk/basic-auth-pw-omitted/option/request_option.go index cf173ed05305..cf94c20781c6 100644 --- a/seed/go-sdk/basic-auth-optional/option/request_option.go +++ b/seed/go-sdk/basic-auth-pw-omitted/option/request_option.go @@ -3,7 +3,7 @@ package option import ( - core "github.com/basic-auth-optional/fern/core" + core "github.com/basic-auth-pw-omitted/fern/core" http "net/http" url "net/url" ) diff --git a/seed/go-sdk/basic-auth-optional/pointer.go b/seed/go-sdk/basic-auth-pw-omitted/pointer.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/pointer.go rename to seed/go-sdk/basic-auth-pw-omitted/pointer.go diff --git a/seed/go-sdk/basic-auth-optional/pointer_test.go b/seed/go-sdk/basic-auth-pw-omitted/pointer_test.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/pointer_test.go rename to seed/go-sdk/basic-auth-pw-omitted/pointer_test.go diff --git a/seed/go-sdk/basic-auth-optional/reference.md b/seed/go-sdk/basic-auth-pw-omitted/reference.md similarity index 100% rename from seed/go-sdk/basic-auth-optional/reference.md rename to seed/go-sdk/basic-auth-pw-omitted/reference.md diff --git a/seed/go-sdk/basic-auth-pw-omitted/snippet.json b/seed/go-sdk/basic-auth-pw-omitted/snippet.json new file mode 100644 index 000000000000..39381e17cb59 --- /dev/null +++ b/seed/go-sdk/basic-auth-pw-omitted/snippet.json @@ -0,0 +1,26 @@ +{ + "endpoints": [ + { + "id": { + "path": "/basic-auth", + "method": "GET", + "identifier_override": "endpoint_basic-auth.getWithBasicAuth" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/basic-auth", + "method": "POST", + "identifier_override": "endpoint_basic-auth.postWithBasicAuth" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/go-sdk/basic-auth-optional/types.go b/seed/go-sdk/basic-auth-pw-omitted/types.go similarity index 97% rename from seed/go-sdk/basic-auth-optional/types.go rename to seed/go-sdk/basic-auth-pw-omitted/types.go index fc9f46088535..3cd55567eec5 100644 --- a/seed/go-sdk/basic-auth-optional/types.go +++ b/seed/go-sdk/basic-auth-pw-omitted/types.go @@ -5,7 +5,7 @@ package basicauthoptional import ( json "encoding/json" fmt "fmt" - internal "github.com/basic-auth-optional/fern/internal" + internal "github.com/basic-auth-pw-omitted/fern/internal" big "math/big" ) diff --git a/seed/go-sdk/basic-auth-optional/types_test.go b/seed/go-sdk/basic-auth-pw-omitted/types_test.go similarity index 100% rename from seed/go-sdk/basic-auth-optional/types_test.go rename to seed/go-sdk/basic-auth-pw-omitted/types_test.go diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/definition/api.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml index 8b1d72b0b769..db01794de599 100644 --- a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml @@ -1,4 +1,4 @@ -name: basic-auth-optional +name: basic-auth-pw-omitted auth: Basic auth-schemes: Basic: diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/errors.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/generators.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml index b30d6ed97cd2..fc35e4407493 100644 --- a/test-definitions/fern/apis/basic-auth-optional/generators.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml @@ -9,7 +9,7 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/php-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted go-sdk: generators: - name: fernapi/fern-go-sdk @@ -19,4 +19,4 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/go-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted From 1ec751e45ff16ef0f4c22401e988b7dfde6fc7f8 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:12:56 +0000 Subject: [PATCH 03/12] fix(go-sdk): bump seed fixture IR version to v63 and regenerate seed output Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- seed/go-sdk/basic-auth-pw-omitted/error_codes.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/errors.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/file_param.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/pointer.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/pointer_test.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/types.go | 2 +- seed/go-sdk/basic-auth-pw-omitted/types_test.go | 2 +- .../fern/apis/basic-auth-pw-omitted/generators.yml | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/seed/go-sdk/basic-auth-pw-omitted/error_codes.go b/seed/go-sdk/basic-auth-pw-omitted/error_codes.go index 62042f8cd6dc..c6391184142c 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/error_codes.go +++ b/seed/go-sdk/basic-auth-pw-omitted/error_codes.go @@ -1,6 +1,6 @@ // Code generated by Fern. DO NOT EDIT. -package basicauthoptional +package basicauthpwomitted import ( core "github.com/basic-auth-pw-omitted/fern/core" diff --git a/seed/go-sdk/basic-auth-pw-omitted/errors.go b/seed/go-sdk/basic-auth-pw-omitted/errors.go index b85af2ecd351..422547b60636 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/errors.go +++ b/seed/go-sdk/basic-auth-pw-omitted/errors.go @@ -1,6 +1,6 @@ // Code generated by Fern. DO NOT EDIT. -package basicauthoptional +package basicauthpwomitted import ( json "encoding/json" diff --git a/seed/go-sdk/basic-auth-pw-omitted/file_param.go b/seed/go-sdk/basic-auth-pw-omitted/file_param.go index ee41d1ece30b..1d97bacba966 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/file_param.go +++ b/seed/go-sdk/basic-auth-pw-omitted/file_param.go @@ -1,4 +1,4 @@ -package basicauthoptional +package basicauthpwomitted import ( "io" diff --git a/seed/go-sdk/basic-auth-pw-omitted/pointer.go b/seed/go-sdk/basic-auth-pw-omitted/pointer.go index 9be282560124..477c80097578 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/pointer.go +++ b/seed/go-sdk/basic-auth-pw-omitted/pointer.go @@ -1,4 +1,4 @@ -package basicauthoptional +package basicauthpwomitted import ( "time" diff --git a/seed/go-sdk/basic-auth-pw-omitted/pointer_test.go b/seed/go-sdk/basic-auth-pw-omitted/pointer_test.go index 3769e25c530a..dbe5a9196021 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/pointer_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/pointer_test.go @@ -1,4 +1,4 @@ -package basicauthoptional +package basicauthpwomitted import ( "testing" diff --git a/seed/go-sdk/basic-auth-pw-omitted/types.go b/seed/go-sdk/basic-auth-pw-omitted/types.go index 3cd55567eec5..8833bc125485 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/types.go +++ b/seed/go-sdk/basic-auth-pw-omitted/types.go @@ -1,6 +1,6 @@ // Code generated by Fern. DO NOT EDIT. -package basicauthoptional +package basicauthpwomitted import ( json "encoding/json" diff --git a/seed/go-sdk/basic-auth-pw-omitted/types_test.go b/seed/go-sdk/basic-auth-pw-omitted/types_test.go index b02b34054488..cf9fe668602f 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/types_test.go +++ b/seed/go-sdk/basic-auth-pw-omitted/types_test.go @@ -1,6 +1,6 @@ // Code generated by Fern. DO NOT EDIT. -package basicauthoptional +package basicauthpwomitted import ( json "encoding/json" diff --git a/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml index fc35e4407493..50ee99b47bef 100644 --- a/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml @@ -4,7 +4,7 @@ groups: generators: - name: fernapi/fern-php-sdk version: latest - ir-version: v61 + ir-version: v63 github: token: ${GITHUB_TOKEN} mode: push @@ -14,7 +14,7 @@ groups: generators: - name: fernapi/fern-go-sdk version: latest - ir-version: v61 + ir-version: v63 github: token: ${GITHUB_TOKEN} mode: push From b000b60faab04314028e2575a78a3f8be6ba7d60 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:49:47 +0000 Subject: [PATCH 04/12] fix(go-sdk): handle usernameOmit/passwordOmit in dynamic snippets generator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 47 ++++++++++++------- .../go-sdk/basic-auth-pw-omitted/snippet.json | 6 +-- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index bea9872d9e25..4fd3646f6971 100644 --- a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -356,7 +356,7 @@ export class EndpointSnippetGenerator { } switch (auth.type) { case "basic": - return values.type === "basic" ? [this.getConstructorBasicAuthArg({ auth, values })] : []; + return values.type === "basic" ? this.getConstructorBasicAuthArgs({ auth, values }) : []; case "bearer": return values.type === "bearer" ? [this.getConstructorBearerAuthArg({ auth, values })] : []; case "header": @@ -378,27 +378,40 @@ export class EndpointSnippetGenerator { this.context.errors.add({ severity: Severity.Warning, message }); } - private getConstructorBasicAuthArg({ + private getConstructorBasicAuthArgs({ auth, values }: { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; - }): go.AstNode { - return go.codeblock((writer) => { - writer.writeNode( - go.invokeFunc({ - func: go.typeReference({ - name: "WithBasicAuth", - importPath: this.context.getOptionImportPath() - }), - arguments_: [ - go.TypeInstantiation.string(values.username), - go.TypeInstantiation.string(values.password) - ] - }) - ); - }); + }): go.AstNode[] { + // usernameOmit/passwordOmit may exist in newer IR versions + const authRecord = auth as unknown as Record; + const usernameOmitted = authRecord.usernameOmit === true; + const passwordOmitted = authRecord.passwordOmit === true; + const arguments_: go.AstNode[] = []; + if (!usernameOmitted) { + arguments_.push(go.TypeInstantiation.string(values.username)); + } + if (!passwordOmitted) { + arguments_.push(go.TypeInstantiation.string(values.password)); + } + if (arguments_.length === 0) { + return []; + } + return [ + go.codeblock((writer) => { + writer.writeNode( + go.invokeFunc({ + func: go.typeReference({ + name: "WithBasicAuth", + importPath: this.context.getOptionImportPath() + }), + arguments_ + }) + ); + }) + ]; } private getConstructorBaseUrlArg({ diff --git a/seed/go-sdk/basic-auth-pw-omitted/snippet.json b/seed/go-sdk/basic-auth-pw-omitted/snippet.json index 39381e17cb59..981d169e6384 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/snippet.json +++ b/seed/go-sdk/basic-auth-pw-omitted/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" } }, { @@ -19,8 +19,8 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" } } ] -} \ No newline at end of file +} From 908798f7b24bd4b0cd304f807aca7042b5162210 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:18:26 +0000 Subject: [PATCH 05/12] refactor(go-sdk): simplify omit checks from === true to !! Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 4fd3646f6971..308597514c50 100644 --- a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -387,8 +387,8 @@ export class EndpointSnippetGenerator { }): go.AstNode[] { // usernameOmit/passwordOmit may exist in newer IR versions const authRecord = auth as unknown as Record; - const usernameOmitted = authRecord.usernameOmit === true; - const passwordOmitted = authRecord.passwordOmit === true; + const usernameOmitted = !!authRecord.usernameOmit; + const passwordOmitted = !!authRecord.passwordOmit; const arguments_: go.AstNode[] = []; if (!usernameOmitted) { arguments_.push(go.TypeInstantiation.string(values.username)); From f1c45dd4d777146907f0e63d356eb5406fd1eff5 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:54:07 +0000 Subject: [PATCH 06/12] fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter to dynamic IR The DynamicSnippetsConverter was constructing dynamic BasicAuth with only username and password fields, dropping usernameOmit/passwordOmit from the main IR's BasicAuthScheme. This caused dynamic snippets generators to always include omitted auth fields (e.g. $password) since they couldn't detect the omit flags in the dynamic IR data. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../DynamicSnippetsConverter.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts index b1750ea01c28..5675bc14999b 100644 --- a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts +++ b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts @@ -732,11 +732,22 @@ export class DynamicSnippetsConverter { } const scheme = auth.schemes[0]; switch (scheme.type) { - case "basic": - return DynamicSnippets.Auth.basic({ + case "basic": { + const basicAuth: DynamicSnippets.BasicAuth & { + usernameOmit?: boolean; + passwordOmit?: boolean; + } = { username: this.inflateName(scheme.username), password: this.inflateName(scheme.password) - }); + }; + if (scheme.usernameOmit) { + basicAuth.usernameOmit = scheme.usernameOmit; + } + if (scheme.passwordOmit) { + basicAuth.passwordOmit = scheme.passwordOmit; + } + return DynamicSnippets.Auth.basic(basicAuth); + } case "bearer": return DynamicSnippets.Auth.bearer({ token: this.inflateName(scheme.token) From b21f402661206800a9c822aafecdcc797ee89610 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:10:28 +0000 Subject: [PATCH 07/12] ci: retrigger CI (flaky go-sdk job cancellation) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From 486e150f2424f0deda56603b875e20e2b932ef33 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:40:13 +0000 Subject: [PATCH 08/12] ci: retrigger CI (flaky csharp-sdk and go-sdk job failures) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From cc1c8be3c4c43a7ac9b6e19981bf32193077d509 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Sat, 4 Apr 2026 04:02:33 +0000 Subject: [PATCH 09/12] ci: retrigger CI (3rd attempt - flaky go-sdk/java-sdk job failures) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From cd34294f3e676448bbba9ec97153a1f2ac2ded00 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:54:59 +0000 Subject: [PATCH 10/12] ci: retrigger CI (4th attempt - flaky go-sdk seed-test-results) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> From f91ea26d2c051e5410e78fdd54b4e46e824255d3 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:04:49 +0000 Subject: [PATCH 11/12] fix(go-sdk): always pass two args to WithBasicAuth in dynamic snippets When passwordOmit or usernameOmit is true, pass empty string for the omitted field instead of omitting the argument entirely. The Go SDK's WithBasicAuth function always takes exactly two parameters. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 13 +++++-------- seed/go-sdk/basic-auth-pw-omitted/snippet.json | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 308597514c50..bac443e4ee64 100644 --- a/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -389,16 +389,13 @@ export class EndpointSnippetGenerator { const authRecord = auth as unknown as Record; const usernameOmitted = !!authRecord.usernameOmit; const passwordOmitted = !!authRecord.passwordOmit; - const arguments_: go.AstNode[] = []; - if (!usernameOmitted) { - arguments_.push(go.TypeInstantiation.string(values.username)); - } - if (!passwordOmitted) { - arguments_.push(go.TypeInstantiation.string(values.password)); - } - if (arguments_.length === 0) { + if (usernameOmitted && passwordOmitted) { return []; } + const arguments_: go.AstNode[] = [ + go.TypeInstantiation.string(usernameOmitted ? "" : values.username), + go.TypeInstantiation.string(passwordOmitted ? "" : values.password) + ]; return [ go.codeblock((writer) => { writer.writeNode( diff --git a/seed/go-sdk/basic-auth-pw-omitted/snippet.json b/seed/go-sdk/basic-auth-pw-omitted/snippet.json index 981d169e6384..39381e17cb59 100644 --- a/seed/go-sdk/basic-auth-pw-omitted/snippet.json +++ b/seed/go-sdk/basic-auth-pw-omitted/snippet.json @@ -8,7 +8,7 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.GetWithBasicAuth(\n\tcontext.TODO(),\n)\n" } }, { @@ -19,8 +19,8 @@ }, "snippet": { "type": "go", - "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" + "client": "import (\n\tcontext \"context\"\n\tfernclient \"github.com/basic-auth-pw-omitted/fern/client\"\n\toption \"github.com/basic-auth-pw-omitted/fern/option\"\n)\n\nclient := fernclient.NewClient(\n\toption.WithBasicAuth(\n\t\t\"\u003cYOUR_USERNAME\u003e\",\n\t\t\"\u003cYOUR_PASSWORD\u003e\",\n\t),\n)\nresponse, err := client.BasicAuth.PostWithBasicAuth(\n\tcontext.TODO(),\n\tmap[string]interface{}{\n\t\t\"key\": \"value\",\n\t},\n)\n" } } ] -} +} \ No newline at end of file From 616689527a890e4fb6309d77935545eb8ffd416a Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:45:31 +0000 Subject: [PATCH 12/12] feat(go-sdk): add versions.yml entry for basic auth omit support Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/go/sdk/versions.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index 5870da948cad..88ef43cabf6a 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.34.4 + changelogEntry: + - summary: | + Support omitting username or password from basic auth in dynamic + snippets when configured via `usernameOmit`/`passwordOmit` in the IR. + When one field is omitted, an empty string is passed for that positional + argument. When both are omitted, the `WithBasicAuth` call is skipped + entirely. + type: feat + createdAt: "2026-04-11" + irVersion: 66 + - version: 1.34.3 changelogEntry: - summary: |