From 3c14b11acfc55027169e8143bc0987dde9ab301d Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:56:06 +0000 Subject: [PATCH 01/20] feat(ruby-sdk): support optional username/password in basic auth when configured in IR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 14 +- generators/ruby-v2/sdk/versions.yml | 12 + ...e_errors_UnauthorizedRequestErrorBody.json | 13 ++ .../basic-auth-optional/.fern/metadata.json | 7 + .../.github/workflows/ci.yml | 75 ++++++ .../basic-auth-optional/.gitignore | 1 + .../basic-auth-optional/.rubocop.yml | 69 ++++++ seed/ruby-sdk-v2/basic-auth-optional/Gemfile | 23 ++ .../basic-auth-optional/Gemfile.custom | 14 ++ .../ruby-sdk-v2/basic-auth-optional/README.md | 157 +++++++++++++ seed/ruby-sdk-v2/basic-auth-optional/Rakefile | 20 ++ .../basic-auth-optional/custom.gemspec.rb | 16 ++ .../dynamic-snippets/example0/snippet.rb | 9 + .../dynamic-snippets/example1/snippet.rb | 9 + .../dynamic-snippets/example2/snippet.rb | 9 + .../dynamic-snippets/example3/snippet.rb | 9 + .../dynamic-snippets/example4/snippet.rb | 9 + .../dynamic-snippets/example5/snippet.rb | 9 + .../dynamic-snippets/example6/snippet.rb | 9 + .../basic-auth-optional/lib/seed.rb | 41 ++++ .../lib/seed/basic_auth/client.rb | 77 +++++++ .../basic-auth-optional/lib/seed/client.rb | 27 +++ .../lib/seed/errors/api_error.rb | 8 + .../lib/seed/errors/client_error.rb | 17 ++ .../lib/seed/errors/redirect_error.rb | 8 + .../lib/seed/errors/response_error.rb | 42 ++++ .../lib/seed/errors/server_error.rb | 11 + .../lib/seed/errors/timeout_error.rb | 8 + .../types/unauthorized_request_error_body.rb | 11 + .../seed/internal/errors/constraint_error.rb | 10 + .../lib/seed/internal/errors/type_error.rb | 10 + .../lib/seed/internal/http/base_request.rb | 51 +++++ .../lib/seed/internal/http/raw_client.rb | 214 ++++++++++++++++++ .../iterators/cursor_item_iterator.rb | 28 +++ .../iterators/cursor_page_iterator.rb | 51 +++++ .../seed/internal/iterators/item_iterator.rb | 59 +++++ .../iterators/offset_item_iterator.rb | 30 +++ .../iterators/offset_page_iterator.rb | 83 +++++++ .../lib/seed/internal/json/request.rb | 41 ++++ .../lib/seed/internal/json/serializable.rb | 25 ++ .../internal/multipart/multipart_encoder.rb | 141 ++++++++++++ .../internal/multipart/multipart_form_data.rb | 78 +++++++ .../multipart/multipart_form_data_part.rb | 51 +++++ .../internal/multipart/multipart_request.rb | 40 ++++ .../lib/seed/internal/types/array.rb | 47 ++++ .../lib/seed/internal/types/boolean.rb | 34 +++ .../lib/seed/internal/types/enum.rb | 56 +++++ .../lib/seed/internal/types/hash.rb | 36 +++ .../lib/seed/internal/types/model.rb | 208 +++++++++++++++++ .../lib/seed/internal/types/model/field.rb | 38 ++++ .../lib/seed/internal/types/type.rb | 35 +++ .../lib/seed/internal/types/union.rb | 161 +++++++++++++ .../lib/seed/internal/types/unknown.rb | 15 ++ .../lib/seed/internal/types/utils.rb | 116 ++++++++++ .../basic-auth-optional/lib/seed/version.rb | 5 + .../basic-auth-optional/reference.md | 118 ++++++++++ .../basic-auth-optional/seed.gemspec | 37 +++ .../basic-auth-optional/snippet.json | 0 .../basic-auth-optional/test/custom.test.rb | 15 ++ .../basic-auth-optional/test/test_helper.rb | 3 + .../iterators/test_cursor_item_iterator.rb | 189 ++++++++++++++++ .../iterators/test_offset_item_iterator.rb | 151 ++++++++++++ .../test/unit/internal/types/test_array.rb | 37 +++ .../test/unit/internal/types/test_boolean.rb | 35 +++ .../test/unit/internal/types/test_enum.rb | 42 ++++ .../test/unit/internal/types/test_hash.rb | 50 ++++ .../test/unit/internal/types/test_model.rb | 154 +++++++++++++ .../test/unit/internal/types/test_union.rb | 62 +++++ .../test/unit/internal/types/test_utils.rb | 212 +++++++++++++++++ .../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 ++ 73 files changed, 3582 insertions(+), 4 deletions(-) create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/.gitignore create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/Gemfile create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/README.md create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/Rakefile create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/reference.md create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/snippet.json create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb 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/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 8bd1772c05d4..d7f5938012ab 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -122,21 +122,27 @@ export class RootClientGenerator extends FileGenerator; + const eitherOmitted = scheme.usernameOmit === true || scheme.passwordOmit === true; + const conditionOp = eitherOmitted ? "||" : "&&"; + const usernameExpr = eitherOmitted ? `${usernameName} || ""` : usernameName; + const passwordExpr = eitherOmitted ? `${passwordName} || ""` : passwordName; if (isAuthOptional || basicAuthSchemes.length > 1) { if (i === 0) { - writer.writeLine(`if !${usernameName}.nil? && !${passwordName}.nil?`); + writer.writeLine(`if !${usernameName}.nil? ${conditionOp} !${passwordName}.nil?`); } else { - writer.writeLine(`elsif !${usernameName}.nil? && !${passwordName}.nil?`); + writer.writeLine(`elsif !${usernameName}.nil? ${conditionOp} !${passwordName}.nil?`); } writer.writeLine( - ` headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameName}}:#{${passwordName}}")}"` + ` headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameExpr}}:#{${passwordExpr}}")}"` ); if (i === basicAuthSchemes.length - 1) { writer.writeLine(`end`); } } else { writer.writeLine( - `headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameName}}:#{${passwordName}}")}"` + `headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameExpr}}:#{${passwordExpr}}")}"` ); } } diff --git a/generators/ruby-v2/sdk/versions.yml b/generators/ruby-v2/sdk/versions.yml index 4153536705a4..20591725adac 100644 --- a/generators/ruby-v2/sdk/versions.yml +++ b/generators/ruby-v2/sdk/versions.yml @@ -1,5 +1,17 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 1.1.12 + changelogEntry: + - summary: | + Support optional username and password in basic auth. The SDK now accepts + username-only, password-only, or both credentials. Missing fields are treated + as empty strings (e.g., username-only encodes `username:`, password-only + encodes `:password`). When neither is provided, the Authorization header is + omitted entirely. + type: feat + createdAt: "2026-03-31" + irVersion: 61 + - version: 1.1.11 changelogEntry: - summary: | 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/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json b/seed/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json new file mode 100644 index 000000000000..885f09fc37f2 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json @@ -0,0 +1,7 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-ruby-sdk-v2", + "generatorVersion": "latest", + "originGitCommit": "DUMMY", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml b/seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml new file mode 100644 index 000000000000..72178ea4c8f1 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: ci + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Run Rubocop + run: bundle exec rubocop + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Run Tests + run: bundle exec rake test + + publish: + name: Publish to RubyGems.org + runs-on: ubuntu-latest + needs: [lint, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + + permissions: + id-token: write + contents: write + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Configure RubyGems credentials + uses: rubygems/configure-rubygems-credentials@v1.0.0 + + - name: Build gem + run: bundle exec rake build + + - name: Push gem to RubyGems + run: gem push pkg/*.gem diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.gitignore b/seed/ruby-sdk-v2/basic-auth-optional/.gitignore new file mode 100644 index 000000000000..c111b331371a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/.gitignore @@ -0,0 +1 @@ +*.gem diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml b/seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml new file mode 100644 index 000000000000..75d8f836f2f0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml @@ -0,0 +1,69 @@ +plugins: + - rubocop-minitest + +AllCops: + TargetRubyVersion: 3.3 + NewCops: enable + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/AccessModifierDeclarations: + Enabled: false + +Lint/ConstantDefinitionInBlock: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Layout/LineLength: + Enabled: false + +Naming/VariableNumber: + EnforcedStyle: normalcase + +Style/Documentation: + Enabled: false + +Style/Lambda: + EnforcedStyle: literal + +Minitest/MultipleAssertions: + Enabled: false + +Minitest/UselessAssertion: + Enabled: false + +# Dynamic snippets are code samples for documentation, not standalone Ruby files. +Style/FrozenStringLiteralComment: + Exclude: + - "dynamic-snippets/**/*" + +Layout/FirstHashElementIndentation: + Exclude: + - "dynamic-snippets/**/*" diff --git a/seed/ruby-sdk-v2/basic-auth-optional/Gemfile b/seed/ruby-sdk-v2/basic-auth-optional/Gemfile new file mode 100644 index 000000000000..29b144d77f48 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/Gemfile @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +group :test, :development do + gem "rake", "~> 13.0" + + gem "minitest", "~> 5.16" + gem "minitest-rg" + + gem "rubocop", "~> 1.21" + gem "rubocop-minitest" + + gem "pry" + + gem "webmock" +end + +# Load custom Gemfile configuration if it exists +custom_gemfile = File.join(__dir__, "Gemfile.custom") +eval_gemfile(custom_gemfile) if File.exist?(custom_gemfile) diff --git a/seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom b/seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom new file mode 100644 index 000000000000..11bdfaf13f2d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Custom Gemfile configuration file +# This file is automatically loaded by the main Gemfile. You can add custom gems, +# groups, or other Gemfile configurations here. If you do make changes to this file, +# you will need to add it to the .fernignore file to prevent your changes from being +# overwritten by the generator. + +# Example usage: +# group :test, :development do +# gem 'custom-gem', '~> 2.0' +# end + +# Add your custom gem dependencies here \ No newline at end of file diff --git a/seed/ruby-sdk-v2/basic-auth-optional/README.md b/seed/ruby-sdk-v2/basic-auth-optional/README.md new file mode 100644 index 000000000000..39dcc2ae12d7 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/README.md @@ -0,0 +1,157 @@ +# Seed Ruby 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%2FRuby) + +The Seed Ruby library provides convenient access to the Seed APIs from Ruby. + +## Table of Contents + +- [Reference](#reference) +- [Usage](#usage) +- [Environments](#environments) +- [Errors](#errors) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) +- [Contributing](#contributing) + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```ruby +require "seed" + +client = Seed::Client.new( + username: "", + password: "" +) + +client.basic_auth.post_with_basic_auth +``` + +## Environments + +This SDK allows you to configure different custom URLs for API requests. You can specify your own custom URL. + +### Custom URL +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com" +) +``` + +## Errors + +Failed API calls will raise errors that can be rescued from granularly. + +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com" +) + +begin + result = client.basic_auth.post_with_basic_auth +rescue Seed::Errors::TimeoutError + puts "API didn't respond before our timeout elapsed" +rescue Seed::Errors::ServiceUnavailableError + puts "API returned status 503, is probably overloaded, try again later" +rescue Seed::Errors::ServerError + puts "API returned some other 5xx status, this is probably a bug" +rescue Seed::Errors::ResponseError => e + puts "API returned an unexpected status other than 5xx: #{e.code} #{e.message}" +rescue Seed::Errors::ApiError => e + puts "Some other error occurred when calling the API: #{e.message}" +end +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries. 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) + +Use the `max_retries` option to configure this behavior. + +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com", + max_retries: 3 # Configure max retries (default is 2) +) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. Use the `timeout` option to configure this behavior. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + timeout: 30 # 30 second timeout +) +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `additional_headers` request option. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + request_options: { + additional_headers: { + "X-Custom-Header" => "custom-value" + } + } +) +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `additional_query_parameters` request option. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + request_options: { + additional_query_parameters: { + "custom_param" => "custom-value" + } + } +) +``` + +## 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/ruby-sdk-v2/basic-auth-optional/Rakefile b/seed/ruby-sdk-v2/basic-auth-optional/Rakefile new file mode 100644 index 000000000000..9bdd4a6ce80b --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/Rakefile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "minitest/test_task" + +Minitest::TestTask.create + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[test] + +task lint: %i[rubocop] + +# Run only the custom test file +Minitest::TestTask.create(:customtest) do |t| + t.libs << "test" + t.test_globs = ["test/custom.test.rb"] +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb b/seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb new file mode 100644 index 000000000000..86d8efd3cd3c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Custom gemspec configuration file +# This file is automatically loaded by the main gemspec file. The 'spec' variable is available +# in this context from the main gemspec file. You can modify this file to add custom metadata, +# dependencies, or other gemspec configurations. If you do make changes to this file, you will +# need to add it to the .fernignore file to prevent your changes from being overwritten. + +def add_custom_gemspec_data(spec) + # Example custom configurations (uncomment and modify as needed) + + # spec.authors = ["Your name"] + # spec.email = ["your.email@example.com"] + # spec.homepage = "https://github.com/your-org/seed-ruby" + # spec.license = "Your license" +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb new file mode 100644 index 000000000000..2d4035b00456 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb new file mode 100644 index 000000000000..2d4035b00456 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb new file mode 100644 index 000000000000..2d4035b00456 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb new file mode 100644 index 000000000000..37fb27d10d1a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb new file mode 100644 index 000000000000..37fb27d10d1a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb new file mode 100644 index 000000000000..37fb27d10d1a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb new file mode 100644 index 000000000000..37fb27d10d1a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb @@ -0,0 +1,9 @@ +require "seed" + +client = Seed::Client.new( + username: "", + password: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb new file mode 100644 index 000000000000..3b8182547ec3 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "json" +require "net/http" +require "securerandom" +require "base64" + +require_relative "seed/internal/json/serializable" +require_relative "seed/internal/types/type" +require_relative "seed/internal/types/utils" +require_relative "seed/internal/types/union" +require_relative "seed/internal/errors/constraint_error" +require_relative "seed/internal/errors/type_error" +require_relative "seed/internal/http/base_request" +require_relative "seed/internal/json/request" +require_relative "seed/internal/http/raw_client" +require_relative "seed/internal/multipart/multipart_encoder" +require_relative "seed/internal/multipart/multipart_form_data_part" +require_relative "seed/internal/multipart/multipart_form_data" +require_relative "seed/internal/multipart/multipart_request" +require_relative "seed/internal/types/model/field" +require_relative "seed/internal/types/model" +require_relative "seed/internal/types/array" +require_relative "seed/internal/types/boolean" +require_relative "seed/internal/types/enum" +require_relative "seed/internal/types/hash" +require_relative "seed/internal/types/unknown" +require_relative "seed/errors/api_error" +require_relative "seed/errors/response_error" +require_relative "seed/errors/client_error" +require_relative "seed/errors/redirect_error" +require_relative "seed/errors/server_error" +require_relative "seed/errors/timeout_error" +require_relative "seed/internal/iterators/item_iterator" +require_relative "seed/internal/iterators/cursor_item_iterator" +require_relative "seed/internal/iterators/offset_item_iterator" +require_relative "seed/internal/iterators/cursor_page_iterator" +require_relative "seed/internal/iterators/offset_page_iterator" +require_relative "seed/errors/types/unauthorized_request_error_body" +require_relative "seed/client" +require_relative "seed/basic_auth/client" diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb new file mode 100644 index 000000000000..e90716fb53a6 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Seed + module BasicAuth + class Client + # @param client [Seed::Internal::Http::RawClient] + # + # @return [void] + def initialize(client:) + @client = client + end + + # GET request with basic auth scheme + # + # @param request_options [Hash] + # @param params [Hash] + # @option request_options [String] :base_url + # @option request_options [Hash{String => Object}] :additional_headers + # @option request_options [Hash{String => Object}] :additional_query_parameters + # @option request_options [Hash{String => Object}] :additional_body_parameters + # @option request_options [Integer] :timeout_in_seconds + # + # @return [Boolean] + def get_with_basic_auth(request_options: {}, **params) + Seed::Internal::Types::Utils.normalize_keys(params) + request = Seed::Internal::JSON::Request.new( + base_url: request_options[:base_url], + method: "GET", + path: "basic-auth", + request_options: request_options + ) + begin + response = @client.send(request) + rescue Net::HTTPRequestTimeout + raise Seed::Errors::TimeoutError + end + code = response.code.to_i + return if code.between?(200, 299) + + error_class = Seed::Errors::ResponseError.subclass_for_code(code) + raise error_class.new(response.body, code: code) + end + + # POST request with basic auth scheme + # + # @param request_options [Hash] + # @param params [Hash] + # @option request_options [String] :base_url + # @option request_options [Hash{String => Object}] :additional_headers + # @option request_options [Hash{String => Object}] :additional_query_parameters + # @option request_options [Hash{String => Object}] :additional_body_parameters + # @option request_options [Integer] :timeout_in_seconds + # + # @return [Boolean] + def post_with_basic_auth(request_options: {}, **params) + params = Seed::Internal::Types::Utils.normalize_keys(params) + request = Seed::Internal::JSON::Request.new( + base_url: request_options[:base_url], + method: "POST", + path: "basic-auth", + body: params, + request_options: request_options + ) + begin + response = @client.send(request) + rescue Net::HTTPRequestTimeout + raise Seed::Errors::TimeoutError + end + code = response.code.to_i + return if code.between?(200, 299) + + error_class = Seed::Errors::ResponseError.subclass_for_code(code) + raise error_class.new(response.body, code: code) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb new file mode 100644 index 000000000000..aa7884f2eb40 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Seed + class Client + # @param base_url [String, nil] + # @param username [String] + # @param password [String] + # + # @return [void] + def initialize(username:, password:, base_url: nil) + headers = { + "User-Agent" => "fern_basic-auth-optional/0.0.1", + "X-Fern-Language" => "Ruby" + } + headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" + @raw_client = Seed::Internal::Http::RawClient.new( + base_url: base_url, + headers: headers + ) + end + + # @return [Seed::BasicAuth::Client] + def basic_auth + @basic_auth ||= Seed::BasicAuth::Client.new(client: @raw_client) + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb new file mode 100644 index 000000000000..b8ba53889b36 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ApiError < StandardError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb new file mode 100644 index 000000000000..c3c6033641e2 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ClientError < ResponseError + end + + class UnauthorizedError < ClientError + end + + class ForbiddenError < ClientError + end + + class NotFoundError < ClientError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb new file mode 100644 index 000000000000..f663c01e7615 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class RedirectError < ResponseError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb new file mode 100644 index 000000000000..beb4a1baf959 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ResponseError < ApiError + attr_reader :code + + def initialize(msg, code:) + @code = code + super(msg) + end + + def inspect + "#<#{self.class.name} @code=#{code} @body=#{message}>" + end + + # Returns the most appropriate error class for the given code. + # + # @return [Class] + def self.subclass_for_code(code) + case code + when 300..399 + RedirectError + when 401 + UnauthorizedError + when 403 + ForbiddenError + when 404 + NotFoundError + when 400..499 + ClientError + when 503 + ServiceUnavailableError + when 500..599 + ServerError + else + ResponseError + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb new file mode 100644 index 000000000000..1838027cdeab --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ServerError < ResponseError + end + + class ServiceUnavailableError < ApiError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb new file mode 100644 index 000000000000..ec3a24bb7e96 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class TimeoutError < ApiError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb new file mode 100644 index 000000000000..a3caea8d9bea --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Seed + module Errors + module Types + class UnauthorizedRequestErrorBody < Internal::Types::Model + field :message, -> { String }, optional: false, nullable: false + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb new file mode 100644 index 000000000000..e2f0bd66ac37 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Errors + class ConstraintError < StandardError + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb new file mode 100644 index 000000000000..6aec80f59f05 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Errors + class TypeError < StandardError + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb new file mode 100644 index 000000000000..d35df463e5b0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Http + # @api private + class BaseRequest + attr_reader :base_url, :path, :method, :headers, :query, :request_options + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [String] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, request_options: {}) + @base_url = base_url + @path = path + @method = method + @headers = headers + @query = query + @request_options = request_options + end + + # @return [Hash] The query parameters merged with additional query parameters from request options. + def encode_query + additional_query = @request_options&.dig(:additional_query_parameters) || @request_options&.dig("additional_query_parameters") || {} + @query.merge(additional_query) + end + + # Child classes should implement: + # - encode_headers: Returns the encoded HTTP request headers. + # - encode_body: Returns the encoded HTTP request body. + + private + + # Merges additional_headers from request_options into sdk_headers, filtering out + # any keys that collide with SDK-set or client-protected headers (case-insensitive). + # @param sdk_headers [Hash] Headers set by the SDK for this request type. + # @param protected_keys [Array] Additional header keys that must not be overridden. + # @return [Hash] The merged headers. + def merge_additional_headers(sdk_headers, protected_keys: []) + additional_headers = @request_options&.dig(:additional_headers) || @request_options&.dig("additional_headers") || {} + all_protected = (sdk_headers.keys + protected_keys).to_set { |k| k.to_s.downcase } + filtered = additional_headers.reject { |key, _| all_protected.include?(key.to_s.downcase) } + sdk_headers.merge(filtered) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb new file mode 100644 index 000000000000..482ab9517714 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Http + # @api private + class RawClient + # Default HTTP status codes that trigger a retry + RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504, 521, 522, 524].freeze + # Initial delay between retries in seconds + INITIAL_RETRY_DELAY = 0.5 + # Maximum delay between retries in seconds + MAX_RETRY_DELAY = 60.0 + # Jitter factor for randomizing retry delays (20%) + JITTER_FACTOR = 0.2 + + # @return [String] The base URL for requests + attr_reader :base_url + + # @param base_url [String] The base url for the request. + # @param max_retries [Integer] The number of times to retry a failed request, defaults to 2. + # @param timeout [Float] The timeout for the request, defaults to 60.0 seconds. + # @param headers [Hash] The headers for the request. + def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}) + @base_url = base_url + @max_retries = max_retries + @timeout = timeout + @default_headers = { + "X-Fern-Language": "Ruby", + "X-Fern-SDK-Name": "seed", + "X-Fern-SDK-Version": "0.0.1" + }.merge(headers) + end + + # @param request [Seed::Internal::Http::BaseRequest] The HTTP request. + # @return [HTTP::Response] The HTTP response. + def send(request) + url = build_url(request) + attempt = 0 + response = nil + + loop do + http_request = build_http_request( + url:, + method: request.method, + headers: request.encode_headers(protected_keys: @default_headers.keys), + body: request.encode_body + ) + + conn = connect(url) + conn.open_timeout = @timeout + conn.read_timeout = @timeout + conn.write_timeout = @timeout + conn.continue_timeout = @timeout + + response = conn.request(http_request) + + break unless should_retry?(response, attempt) + + delay = retry_delay(response, attempt) + sleep(delay) + attempt += 1 + end + + response + end + + # Determines if a request should be retried based on the response status code. + # @param response [Net::HTTPResponse] The HTTP response. + # @param attempt [Integer] The current retry attempt (0-indexed). + # @return [Boolean] Whether the request should be retried. + def should_retry?(response, attempt) + return false if attempt >= @max_retries + + status = response.code.to_i + RETRYABLE_STATUSES.include?(status) + end + + # Calculates the delay before the next retry attempt using exponential backoff with jitter. + # Respects Retry-After header if present. + # @param response [Net::HTTPResponse] The HTTP response. + # @param attempt [Integer] The current retry attempt (0-indexed). + # @return [Float] The delay in seconds before the next retry. + def retry_delay(response, attempt) + # Check for Retry-After header (can be seconds or HTTP date) + retry_after = response["Retry-After"] + if retry_after + delay = parse_retry_after(retry_after) + return [delay, MAX_RETRY_DELAY].min if delay&.positive? + end + + # Exponential backoff with jitter: base_delay * 2^attempt + base_delay = INITIAL_RETRY_DELAY * (2**attempt) + add_jitter([base_delay, MAX_RETRY_DELAY].min) + end + + # Parses the Retry-After header value. + # @param value [String] The Retry-After header value (seconds or HTTP date). + # @return [Float, nil] The delay in seconds, or nil if parsing fails. + def parse_retry_after(value) + # Try parsing as integer (seconds) + seconds = Integer(value, exception: false) + return seconds.to_f if seconds + + # Try parsing as HTTP date + begin + retry_time = Time.httpdate(value) + delay = retry_time - Time.now + delay.positive? ? delay : nil + rescue ArgumentError + nil + end + end + + # Adds random jitter to a delay value. + # @param delay [Float] The base delay in seconds. + # @return [Float] The delay with jitter applied. + def add_jitter(delay) + jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2 + [delay + jitter, 0].max + end + + LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze + + # @param request [Seed::Internal::Http::BaseRequest] The HTTP request. + # @return [URI::Generic] The URL. + def build_url(request) + encoded_query = request.encode_query + + # If the path is already an absolute URL, use it directly + if request.path.start_with?("http://", "https://") + url = request.path + url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any? + parsed = URI.parse(url) + validate_https!(parsed) + return parsed + end + + path = request.path.start_with?("/") ? request.path[1..] : request.path + base = request.base_url || @base_url + url = "#{base.chomp("/")}/#{path}" + url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any? + parsed = URI.parse(url) + validate_https!(parsed) + parsed + end + + # Raises if the URL uses http:// for a non-localhost host, which would + # send authentication credentials in plaintext. + # @param url [URI::Generic] The parsed URL. + def validate_https!(url) + return if url.scheme != "http" + return if LOCALHOST_HOSTS.include?(url.host) + + raise ArgumentError, "Refusing to send request to non-HTTPS URL: #{url}. " \ + "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL." + end + + # @param url [URI::Generic] The url to the resource. + # @param method [String] The HTTP method to use. + # @param headers [Hash] The headers for the request. + # @param body [String, nil] The body for the request. + # @return [HTTP::Request] The HTTP request. + def build_http_request(url:, method:, headers: {}, body: nil) + request = Net::HTTPGenericRequest.new( + method, + !body.nil?, + method != "HEAD", + url + ) + + request_headers = @default_headers.merge(headers) + request_headers.each { |name, value| request[name] = value } + request.body = body if body + + request + end + + # @param query [Hash] The query for the request. + # @return [String, nil] The encoded query. + def encode_query(query) + query.to_h.empty? ? nil : URI.encode_www_form(query) + end + + # @param url [URI::Generic] The url to connect to. + # @return [Net::HTTP] The HTTP connection. + def connect(url) + is_https = (url.scheme == "https") + + port = if url.port + url.port + elsif is_https + Net::HTTP.https_default_port + else + Net::HTTP.http_default_port + end + + http = Net::HTTP.new(url.host, port) + http.use_ssl = is_https + http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https + # NOTE: We handle retries at the application level with HTTP status code awareness, + # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries. + http.max_retries = 0 + http + end + + # @return [String] + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb new file mode 100644 index 000000000000..ab627ffc7025 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Seed + module Internal + class CursorItemIterator < ItemIterator + # Instantiates a CursorItemIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields individual items from it. + # + # @param initial_cursor [String] The initial cursor to use when iterating, if any. + # @param cursor_field [Symbol] The field in API responses to extract the next cursor from. + # @param item_field [Symbol] The field in API responses to extract the items to iterate over. + # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API. + # @return [Seed::Internal::CursorItemIterator] + def initialize(initial_cursor:, cursor_field:, item_field:, &) + super() + @item_field = item_field + @page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &) + @page = nil + end + + # Returns the CursorPageIterator mediating access to the underlying API. + # + # @return [Seed::Internal::CursorPageIterator] + def pages + @page_iterator + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb new file mode 100644 index 000000000000..f479a749fef9 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Seed + module Internal + class CursorPageIterator + include Enumerable + + # Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields pages of items. + # + # @param initial_cursor [String] The initial cursor to use when iterating, if any. + # @param cursor_field [Symbol] The name of the field in API responses to extract the next cursor from. + # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API. + # @return [Seed::Internal::CursorPageIterator] + def initialize(initial_cursor:, cursor_field:, &block) + @need_initial_load = initial_cursor.nil? + @cursor = initial_cursor + @cursor_field = cursor_field + @get_next_page = block + end + + # Iterates over each page returned by the API. + # + # @param block [Proc] The block which each retrieved page is yielded to. + # @return [NilClass] + def each(&block) + while (page = next_page) + block.call(page) + end + end + + # Whether another page will be available from the API. + # + # @return [Boolean] + def next? + @need_initial_load || !@cursor.nil? + end + + # Retrieves the next page from the API. + # + # @return [Boolean] + def next_page + return if !@need_initial_load && @cursor.nil? + + @need_initial_load = false + fetched_page = @get_next_page.call(@cursor) + @cursor = fetched_page.send(@cursor_field) + fetched_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb new file mode 100644 index 000000000000..1284fb0fd367 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Seed + module Internal + class ItemIterator + include Enumerable + + # Iterates over each item returned by the API. + # + # @param block [Proc] The block which each retrieved item is yielded to. + # @return [NilClass] + def each(&block) + while (item = next_element) + block.call(item) + end + end + + # Whether another item will be available from the API. + # + # @return [Boolean] + def next? + load_next_page if @page.nil? + return false if @page.nil? + + return true if any_items_in_cached_page? + + load_next_page + any_items_in_cached_page? + end + + # Retrieves the next item from the API. + def next_element + item = next_item_from_cached_page + return item if item + + load_next_page + next_item_from_cached_page + end + + private + + def next_item_from_cached_page + return unless @page + + @page.send(@item_field).shift + end + + def any_items_in_cached_page? + return false unless @page + + !@page.send(@item_field).empty? + end + + def load_next_page + @page = @page_iterator.next_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb new file mode 100644 index 000000000000..f8840246686d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Seed + module Internal + class OffsetItemIterator < ItemIterator + # Instantiates an OffsetItemIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields the individual items from it. + # + # @param initial_page [Integer] The initial page or offset to start from when iterating. + # @param item_field [Symbol] The name of the field in API responses to extract the items to iterate over. + # @param has_next_field [Symbol] The name of the field in API responses containing a boolean of whether another page exists. + # @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1) + # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API. + # + # @return [Seed::Internal::OffsetItemIterator] + def initialize(initial_page:, item_field:, has_next_field:, step:, &) + super() + @item_field = item_field + @page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &) + @page = nil + end + + # Returns the OffsetPageIterator that is mediating access to the underlying API. + # + # @return [Seed::Internal::OffsetPageIterator] + def pages + @page_iterator + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb new file mode 100644 index 000000000000..051b65c5774c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Seed + module Internal + class OffsetPageIterator + include Enumerable + + # Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it. + # + # @param initial_page [Integer] The initial page to use when iterating, if any. + # @param item_field [Symbol] The field to pull the list of items to iterate over. + # @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any. + # @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1) + # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API. + # @return [Seed::Internal::OffsetPageIterator] + def initialize(initial_page:, item_field:, has_next_field:, step:, &block) + @page_number = initial_page || (step ? 0 : 1) + @item_field = item_field + @has_next_field = has_next_field + @step = step + @get_next_page = block + + # A cache of whether the API has another page, if it gives us that information... + @next_page = nil + # ...or the actual next page, preloaded, if it doesn't. + @has_next_page = nil + end + + # Iterates over each page returned by the API. + # + # @param block [Proc] The block which each retrieved page is yielded to. + # @return [NilClass] + def each(&block) + while (page = next_page) + block.call(page) + end + end + + # Whether another page will be available from the API. + # + # @return [Boolean] + def next? + return @has_next_page unless @has_next_page.nil? + return true if @next_page + + fetched_page = @get_next_page.call(@page_number) + fetched_page_items = fetched_page&.send(@item_field) + if fetched_page_items.nil? || fetched_page_items.empty? + @has_next_page = false + else + @next_page = fetched_page + true + end + end + + # Returns the next page from the API. + def next_page + return nil if @page_number.nil? + + if @next_page + this_page = @next_page + @next_page = nil + else + this_page = @get_next_page.call(@page_number) + end + + @has_next_page = this_page&.send(@has_next_field) if @has_next_field + + items = this_page.send(@item_field) + if items.nil? || items.empty? + @page_number = nil + return nil + elsif @step + @page_number += items.length + else + @page_number += 1 + end + + this_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb new file mode 100644 index 000000000000..667ceae8ac59 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Seed + module Internal + module JSON + # @api private + class Request < Seed::Internal::Http::BaseRequest + attr_reader :body + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [Symbol] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param body [Object, nil] The JSON request body (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {}) + super(base_url:, path:, method:, headers:, query:, request_options:) + + @body = body + end + + # @return [Hash] The encoded HTTP request headers. + # @param protected_keys [Array] Header keys set by the SDK client (e.g. auth, metadata) + # that must not be overridden by additional_headers from request_options. + def encode_headers(protected_keys: []) + sdk_headers = { + "Content-Type" => "application/json", + "Accept" => "application/json" + }.merge(@headers) + merge_additional_headers(sdk_headers, protected_keys:) + end + + # @return [String, nil] The encoded HTTP request body. + def encode_body + @body.nil? ? nil : ::JSON.generate(@body) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb new file mode 100644 index 000000000000..f80a15fb962c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Seed + module Internal + module JSON + module Serializable + # Loads data from JSON into its deserialized form + # + # @param str [String] Raw JSON to load into an object + # @return [Object] + def load(str) + raise NotImplementedError + end + + # Dumps data from its deserialized form into JSON + # + # @param value [Object] The deserialized value + # @return [String] + def dump(value) + raise NotImplementedError + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb new file mode 100644 index 000000000000..307ad7436a57 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # Encodes parameters into a `multipart/form-data` payload as described by RFC + # 2388: + # + # https://tools.ietf.org/html/rfc2388 + # + # This is most useful for transferring file-like objects. + # + # Parameters should be added with `#encode`. When ready, use `#body` to get + # the encoded result and `#content_type` to get the value that should be + # placed in the `Content-Type` header of a subsequent request (which includes + # a boundary value). + # + # This abstraction is heavily inspired by Stripe's multipart/form-data implementation, + # which can be found here: + # + # https://github.com/stripe/stripe-ruby/blob/ca00b676f04ac421cf5cb5ff0325f243651677b6/lib/stripe/multipart_encoder.rb#L18 + # + # @api private + class Encoder + CONTENT_TYPE = "multipart/form-data" + CRLF = "\r\n" + + attr_reader :boundary, :body + + def initialize + # Chose the same number of random bytes that Go uses in its standard + # library implementation. Easily enough entropy to ensure that it won't + # be present in a file we're sending. + @boundary = SecureRandom.hex(30) + + @body = String.new + @closed = false + @first_field = true + end + + # Gets the content type string including the boundary. + # + # @return [String] The content type with boundary + def content_type + "#{CONTENT_TYPE}; boundary=#{@boundary}" + end + + # Encode the given FormData object into a multipart/form-data payload. + # + # @param form_data [FormData] The form data to encode + # @return [String] The encoded body. + def encode(form_data) + return "" if form_data.parts.empty? + + form_data.parts.each do |part| + write_part(part) + end + close + + @body + end + + # Writes a FormDataPart to the encoder. + # + # @param part [FormDataPart] The part to write + # @return [nil] + def write_part(part) + raise "Cannot write to closed encoder" if @closed + + write_field( + name: part.name, + data: part.contents, + filename: part.filename, + headers: part.headers + ) + + nil + end + + # Writes a field to the encoder. + # + # @param name [String] The field name + # @param data [String] The field data + # @param filename [String, nil] Optional filename + # @param headers [Hash, nil] Optional additional headers + # @return [nil] + def write_field(name:, data:, filename: nil, headers: nil) + raise "Cannot write to closed encoder" if @closed + + if @first_field + @first_field = false + else + @body << CRLF + end + + @body << "--#{@boundary}#{CRLF}" + @body << %(Content-Disposition: form-data; name="#{escape(name.to_s)}") + @body << %(; filename="#{escape(filename)}") if filename + @body << CRLF + + if headers + headers.each do |key, value| + @body << "#{key}: #{value}#{CRLF}" + end + elsif filename + # Default content type for files. + @body << "Content-Type: application/octet-stream#{CRLF}" + end + + @body << CRLF + @body << data.to_s + + nil + end + + # Finalizes the encoder by writing the final boundary. + # + # @return [nil] + def close + raise "Encoder already closed" if @closed + + @body << CRLF + @body << "--#{@boundary}--" + @closed = true + + nil + end + + private + + # Escapes quotes for use in header values and replaces line breaks with spaces. + # + # @param str [String] The string to escape + # @return [String] The escaped string + def escape(str) + str.to_s.gsub('"', "%22").tr("\n", " ").tr("\r", " ") + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb new file mode 100644 index 000000000000..5be1bb25341f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # @api private + class FormData + # @return [Array] The parts in this multipart form data. + attr_reader :parts + + # @return [Encoder] The encoder for this multipart form data. + private attr_reader :encoder + + def initialize + @encoder = Encoder.new + @parts = [] + end + + # Adds a new part to the multipart form data. + # + # @param name [String] The name of the form field + # @param value [String, Integer, Float, Boolean, #read] The value of the field + # @param content_type [String, nil] Optional content type + # @return [self] Returns self for chaining + def add(name:, value:, content_type: nil) + headers = content_type ? { "Content-Type" => content_type } : nil + add_part(FormDataPart.new(name:, value:, headers:)) + end + + # Adds a file to the multipart form data. + # + # @param name [String] The name of the form field + # @param file [#read] The file or readable object + # @param filename [String, nil] Optional filename (defaults to basename of path for File objects) + # @param content_type [String, nil] Optional content type (e.g. "image/png") + # @return [self] Returns self for chaining + def add_file(name:, file:, filename: nil, content_type: nil) + headers = content_type ? { "Content-Type" => content_type } : nil + filename ||= filename_for(file) + add_part(FormDataPart.new(name:, value: file, filename:, headers:)) + end + + # Adds a pre-created part to the multipart form data. + # + # @param part [FormDataPart] The part to add + # @return [self] Returns self for chaining + def add_part(part) + @parts << part + self + end + + # Gets the content type string including the boundary. + # + # @return [String] The content type with boundary. + def content_type + @encoder.content_type + end + + # Encode the multipart form data into a multipart/form-data payload. + # + # @return [String] The encoded body. + def encode + @encoder.encode(self) + end + + private + + def filename_for(file) + if file.is_a?(::File) || file.respond_to?(:path) + ::File.basename(file.path) + elsif file.respond_to?(:name) + file.name + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb new file mode 100644 index 000000000000..de45416ee087 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "securerandom" + +module Seed + module Internal + module Multipart + # @api private + class FormDataPart + attr_reader :name, :contents, :filename, :headers + + # @param name [String] The name of the form field + # @param value [String, Integer, Float, Boolean, File, #read] The value of the field + # @param filename [String, nil] Optional filename for file uploads + # @param headers [Hash, nil] Optional additional headers + def initialize(name:, value:, filename: nil, headers: nil) + @name = name + @contents = convert_to_content(value) + @filename = filename + @headers = headers + end + + # Converts the part to a hash suitable for serialization. + # + # @return [Hash] A hash representation of the part + def to_hash + result = { + name: @name, + contents: @contents + } + result[:filename] = @filename if @filename + result[:headers] = @headers if @headers + result + end + + private + + # Converts various types of values to a content representation + # @param value [String, Integer, Float, Boolean, #read] The value to convert + # @return [String] The string representation of the value + def convert_to_content(value) + if value.respond_to?(:read) + value.read + else + value.to_s + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb new file mode 100644 index 000000000000..9fa80cee01ab --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # @api private + class Request < Seed::Internal::Http::BaseRequest + attr_reader :body + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [Symbol] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param body [MultipartFormData, nil] The multipart form data for the request (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {}) + super(base_url:, path:, method:, headers:, query:, request_options:) + + @body = body + end + + # @return [Hash] The encoded HTTP request headers. + # @param protected_keys [Array] Header keys set by the SDK client (e.g. auth, metadata) + # that must not be overridden by additional_headers from request_options. + def encode_headers(protected_keys: []) + sdk_headers = { + "Content-Type" => @body.content_type + }.merge(@headers) + merge_additional_headers(sdk_headers, protected_keys:) + end + + # @return [String, nil] The encoded HTTP request body. + def encode_body + @body&.encode + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb new file mode 100644 index 000000000000..f3c7c1bd9549 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # An array of a specific type + class Array + include Seed::Internal::Types::Type + + attr_reader :type + + class << self + # Instantiates a new `Array` of a given type + # + # @param type [Object] The member type of this array + # + # @return [Seed::Internal::Types::Array] + def [](type) + new(type) + end + end + + # @api private + def initialize(type) + @type = type + end + + # Coerces a value into this array + # + # @param value [Object] + # @option strict [Boolean] + # @return [::Array] + def coerce(value, strict: strict?) + unless value.is_a?(::Array) + raise Errors::TypeError, "cannot coerce `#{value.class}` to Array<#{type}>" if strict + + return value + end + + value.map do |element| + Utils.coerce(type, element, strict: strict) + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb new file mode 100644 index 000000000000..d4e3277e566f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + module Boolean + extend Seed::Internal::Types::Union + + member TrueClass + member FalseClass + + # Overrides the base coercion method for enums to allow integer and string values to become booleans + # + # @param value [Object] + # @option strict [Boolean] + # @return [Object] + def self.coerce(value, strict: strict?) + case value + when TrueClass, FalseClass + return value + when Integer + return value == 1 + when String + return %w[1 true].include?(value) + end + + raise Errors::TypeError, "cannot coerce `#{value.class}` to Boolean" if strict + + value + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb new file mode 100644 index 000000000000..72e45e4c1f27 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Module for defining enums + module Enum + include Type + + # @api private + # + # @return [Array] + def values + @values ||= constants.map { |c| const_get(c) } + end + + # @api private + def finalize! + values + end + + # @api private + def strict? + @strict ||= false + end + + # @api private + def strict! + @strict = true + end + + def coerce(value, strict: strict?) + coerced_value = Utils.coerce(Symbol, value) + + return coerced_value if values.include?(coerced_value) + + raise Errors::TypeError, "`#{value}` not in enum #{self}" if strict + + value + end + + # Parse JSON string and coerce to the enum value + # + # @param str [String] JSON string to parse + # @return [String] The enum value + def load(str) + coerce(::JSON.parse(str)) + end + + def inspect + "#{name}[#{values.join(", ")}]" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb new file mode 100644 index 000000000000..d8bffa63ac11 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + class Hash + include Type + + attr_reader :key_type, :value_type + + class << self + def [](key_type, value_type) + new(key_type, value_type) + end + end + + def initialize(key_type, value_type) + @key_type = key_type + @value_type = value_type + end + + def coerce(value, strict: strict?) + unless value.is_a?(::Hash) + raise Errors::TypeError, "not hash" if strict + + return value + end + + value.to_h do |k, v| + [Utils.coerce(key_type, k, strict: strict), Utils.coerce(value_type, v, strict: strict)] + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb new file mode 100644 index 000000000000..8caca14ff7ea --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # @abstract + # + # An abstract model that all data objects will inherit from + class Model + include Type + + class << self + # The defined fields for this model + # + # @api private + # + # @return [Hash] + def fields + @fields ||= if self < Seed::Internal::Types::Model + superclass.fields.dup + else + {} + end + end + + # Any extra fields that have been created from instantiation + # + # @api private + # + # @return [Hash] + def extra_fields + @extra_fields ||= {} + end + + # Define a new field on this model + # + # @param name [Symbol] The name of the field + # @param type [Class] Type of the field + # @option optional [Boolean] If it is an optional field + # @option nullable [Boolean] If it is a nullable field + # @option api_name [Symbol, String] Name in the API of this field. When serializing/deserializing, will use + # this field name + # @return [void] + def field(name, type, optional: false, nullable: false, api_name: nil, default: nil) + add_field_definition(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name, + default: default) + + define_accessor(name) + define_setter(name) + end + + # Define a new literal for this model + # + # @param name [Symbol] + # @param value [Object] + # @option api_name [Symbol, String] + # @return [void] + def literal(name, value, api_name: nil) + add_field_definition(name: name, type: value.class, optional: false, nullable: false, api_name: api_name, + value: value) + + define_accessor(name) + end + + # Adds a new field definition into the class's fields registry + # + # @api private + # + # @param name [Symbol] + # @param type [Class] + # @option optional [Boolean] + # @return [void] + private def add_field_definition(name:, type:, optional:, nullable:, api_name:, default: nil, value: nil) + fields[name.to_sym] = + Field.new(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name, + value: value, default: default) + end + + # Adds a new field definition into the class's extra fields registry + # + # @api private + # + # @param name [Symbol] + # @param type [Class] + # @option required [Boolean] + # @option optional [Boolean] + # @return [void] + def add_extra_field_definition(name:, type:) + return if extra_fields.key?(name.to_sym) + + extra_fields[name.to_sym] = Field.new(name: name, type: type, optional: true, nullable: false) + + define_accessor(name) + define_setter(name) + end + + # @api private + private def define_accessor(name) + method_name = name.to_sym + + define_method(method_name) do + @data[name] + end + end + + # @api private + private def define_setter(name) + method_name = :"#{name}=" + + define_method(method_name) do |val| + @data[name] = val + end + end + + def coerce(value, strict: (respond_to?(:strict?) ? strict? : false)) # rubocop:disable Lint/UnusedMethodArgument + return value if value.is_a?(self) + + return value unless value.is_a?(::Hash) + + new(value) + end + + def load(str) + coerce(::JSON.parse(str, symbolize_names: true)) + end + + def ===(instance) + instance.class.ancestors.include?(self) + end + end + + # Creates a new instance of this model + # TODO: Should all this logic be in `#coerce` instead? + # + # @param values [Hash] + # @option strict [Boolean] + # @return [self] + def initialize(values = {}) + @data = {} + + values = Utils.symbolize_keys(values.dup) + + self.class.fields.each do |field_name, field| + value = values.delete(field.api_name.to_sym) || values.delete(field.api_name) || values.delete(field_name) + + field_value = value || (if field.literal? + field.value + elsif field.default + field.default + end) + + @data[field_name] = Utils.coerce(field.type, field_value) + end + + # Any remaining values in the input become extra fields + values.each do |name, value| + self.class.add_extra_field_definition(name: name, type: value.class) + + @data[name.to_sym] = value + end + end + + def to_h + result = self.class.fields.merge(self.class.extra_fields).each_with_object({}) do |(name, field), acc| + # If there is a value present in the data, use that value + # If there is a `nil` value present in the data, and it is optional but NOT nullable, exclude key altogether + # If there is a `nil` value present in the data, and it is optional and nullable, use the nil value + + value = @data[name] + + next if value.nil? && field.optional && !field.nullable + + if value.is_a?(::Array) + value = value.map { |item| item.respond_to?(:to_h) ? item.to_h : item } + elsif value.respond_to?(:to_h) + value = value.to_h + end + + acc[field.api_name] = value + end + + # Inject union discriminant if this instance was coerced from a discriminated union + # and the discriminant key is not already present in the result + discriminant_key = instance_variable_get(:@_fern_union_discriminant_key) + discriminant_value = instance_variable_get(:@_fern_union_discriminant_value) + result[discriminant_key] = discriminant_value if discriminant_key && discriminant_value && !result.key?(discriminant_key) + + result + end + + def ==(other) + self.class == other.class && to_h == other.to_h + end + + # @return [String] + def inspect + attrs = @data.map do |name, value| + field = self.class.fields[name] || self.class.extra_fields[name] + display_value = field&.sensitive? ? "[REDACTED]" : value.inspect + "#{name}=#{display_value}" + end + + "#<#{self.class.name}:0x#{object_id&.to_s(16)} #{attrs.join(" ")}>" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb new file mode 100644 index 000000000000..6ce0186f6a5d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + class Model + # Definition of a field on a model + class Field + SENSITIVE_FIELD_NAMES = %i[ + password secret token api_key apikey access_token refresh_token + client_secret client_id credential bearer authorization + ].freeze + + attr_reader :name, :type, :optional, :nullable, :api_name, :value, :default + + def initialize(name:, type:, optional: false, nullable: false, api_name: nil, value: nil, default: nil) + @name = name.to_sym + @type = type + @optional = optional + @nullable = nullable + @api_name = api_name || name.to_s + @value = value + @default = default + end + + def literal? + !value.nil? + end + + def sensitive? + SENSITIVE_FIELD_NAMES.include?(@name) || + SENSITIVE_FIELD_NAMES.any? { |sensitive| @name.to_s.include?(sensitive.to_s) } + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb new file mode 100644 index 000000000000..5866caf1dbda --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # @abstract + module Type + include Seed::Internal::JSON::Serializable + + # Coerces a value to this type + # + # @param value [unknown] + # @option strict [Boolean] If we should strictly coerce this value + def coerce(value, strict: strict?) + raise NotImplementedError + end + + # Returns if strictness is on for this type, defaults to `false` + # + # @return [Boolean] + def strict? + @strict ||= false + end + + # Enable strictness by default for this type + # + # @return [void] + def strict! + @strict = true + self + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb new file mode 100644 index 000000000000..f3e118a2fa78 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Define a union between two types + module Union + include Seed::Internal::Types::Type + + def members + @members ||= [] + end + + # Add a member to this union + # + # @param type [Object] + # @option key [Symbol, String] + # @return [void] + def member(type, key: nil) + members.push([key, Utils.wrap_type(type)]) + self + end + + def type_member?(type) + members.any? { |_key, type_fn| type == type_fn.call } + end + + # Set the discriminant for this union + # + # @param key [Symbol, String] + # @return [void] + def discriminant(key) + @discriminant = key + end + + # @api private + private def discriminated? + !@discriminant.nil? + end + + # Check if value matches a type, handling type wrapper instances + # (Internal::Types::Hash and Internal::Types::Array instances) + # + # @param value [Object] + # @param member_type [Object] + # @return [Boolean] + private def type_matches?(value, member_type) + case member_type + when Seed::Internal::Types::Hash + value.is_a?(::Hash) + when Seed::Internal::Types::Array + value.is_a?(::Array) + when Class, Module + value.is_a?(member_type) + else + false + end + end + + # Resolves the type of a value to be one of the members + # + # @param value [Object] + # @return [Class] + private def resolve_member(value) + if discriminated? && value.is_a?(::Hash) + # Try both symbol and string keys for the discriminant + discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil) + + return if discriminant_value.nil? + + # Convert to string for consistent comparison + discriminant_str = discriminant_value.to_s + + # First try exact match + members_hash = members.to_h + result = members_hash[discriminant_str]&.call + return result if result + + # Try case-insensitive match as fallback + discriminant_lower = discriminant_str.downcase + matching_keys = members_hash.keys.select { |k| k.to_s.downcase == discriminant_lower } + + # Only use case-insensitive match if exactly one key matches (avoid ambiguity) + return members_hash[matching_keys.first]&.call if matching_keys.length == 1 + + nil + else + # First try exact type matching + result = members.find do |_key, mem| + member_type = Utils.unwrap_type(mem) + type_matches?(value, member_type) + end&.last&.call + + return result if result + + # For Hash values, try to coerce into Model member types + if value.is_a?(::Hash) + members.find do |_key, mem| + member_type = Utils.unwrap_type(mem) + # Check if member_type is a Model class + next unless member_type.is_a?(Class) && member_type <= Model + + # Try to coerce the hash into this model type with strict mode + begin + candidate = Utils.coerce(member_type, value, strict: true) + + # Validate that all required (non-optional) fields are present + # This ensures undiscriminated unions properly distinguish between member types + member_type.fields.each do |field_name, field| + raise Errors::TypeError, "Required field `#{field_name}` missing for union member #{member_type.name}" if candidate.instance_variable_get(:@data)[field_name].nil? && !field.optional + end + + true + rescue Errors::TypeError + false + end + end&.last&.call + end + end + end + + def coerce(value, strict: strict?) + type = resolve_member(value) + + unless type + return value unless strict + + if discriminated? + raise Errors::TypeError, + "value of type `#{value.class}` not member of union #{self}" + end + + raise Errors::TypeError, "could not resolve to member of union #{self}" + end + + coerced = Utils.coerce(type, value, strict: strict) + + # For discriminated unions, store the discriminant info on the coerced instance + # so it can be injected back during serialization (to_h) + if discriminated? && value.is_a?(::Hash) && coerced.is_a?(Model) + discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil) + if discriminant_value + coerced.instance_variable_set(:@_fern_union_discriminant_key, @discriminant.to_s) + coerced.instance_variable_set(:@_fern_union_discriminant_value, discriminant_value) + end + end + + coerced + end + + # Parse JSON string and coerce to the correct union member type + # + # @param str [String] JSON string to parse + # @return [Object] Coerced value matching a union member + def load(str) + coerce(::JSON.parse(str, symbolize_names: true)) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb new file mode 100644 index 000000000000..7b58de956da9 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + module Unknown + include Seed::Internal::Types::Type + + def coerce(value) + value + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb new file mode 100644 index 000000000000..5a6eeb23b1b0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Utilities for dealing with and checking types + module Utils + # Wraps a type into a type function + # + # @param type [Proc, Object] + # @return [Proc] + def self.wrap_type(type) + case type + when Proc + type + else + -> { type } + end + end + + # Resolves a type or type function into a type + # + # @param type [Proc, Object] + # @return [Object] + def self.unwrap_type(type) + type.is_a?(Proc) ? type.call : type + end + + def self.coerce(target, value, strict: false) + type = unwrap_type(target) + + case type + in Array + case value + when ::Array + return type.coerce(value, strict: strict) + when Set, ::Hash + return coerce(type, value.to_a) + end + in Hash + case value + when ::Hash + return type.coerce(value, strict: strict) + when ::Array + return coerce(type, value.to_h) + end + in ->(t) { t <= NilClass } + return nil + in ->(t) { t <= String } + case value + when String, Symbol, Numeric, TrueClass, FalseClass + return value.to_s + end + in ->(t) { t <= Symbol } + case value + when Symbol, String + return value.to_sym + end + in ->(t) { t <= Integer } + case value + when Numeric, String, Time + return value.to_i + end + in ->(t) { t <= Float } + case value + when Numeric, Time, String + return value.to_f + end + in ->(t) { t <= Model } + case value + when type + return value + when ::Hash + return type.coerce(value, strict: strict) + end + in Module + case type + in ->(t) { + t.singleton_class.include?(Enum) || + t.singleton_class.include?(Union) + } + return type.coerce(value, strict: strict) + else + value # rubocop:disable Lint/Void + end + else + value # rubocop:disable Lint/Void + end + + raise Errors::TypeError, "cannot coerce value of type `#{value.class}` to `#{target}`" if strict + + value + end + + def self.symbolize_keys(hash) + hash.transform_keys(&:to_sym) + end + + # Converts camelCase keys to snake_case symbols + # This allows SDK methods to accept both snake_case and camelCase keys + # e.g., { refundMethod: ... } becomes { refund_method: ... } + # + # @param hash [Hash] + # @return [Hash] + def self.normalize_keys(hash) + hash.transform_keys do |key| + key_str = key.to_s + # Convert camelCase to snake_case + snake_case = key_str.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase + snake_case.to_sym + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb new file mode 100644 index 000000000000..00dd45cdd958 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Seed + VERSION = "0.0.1" +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/reference.md b/seed/ruby-sdk-v2/basic-auth-optional/reference.md new file mode 100644 index 000000000000..7bfdfbd7ac04 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/reference.md @@ -0,0 +1,118 @@ +# Reference +## BasicAuth +
client.basic_auth.get_with_basic_auth() -> Internal::Types::Boolean +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```ruby +client.basic_auth.get_with_basic_auth +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `Seed::BasicAuth::RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.basic_auth.post_with_basic_auth(request) -> Internal::Types::Boolean +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```ruby +client.basic_auth.post_with_basic_auth +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `Object` + +
+
+ +
+
+ +**request_options:** `Seed::BasicAuth::RequestOptions` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec b/seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec new file mode 100644 index 000000000000..aff5ff0c3c1c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative "lib/seed/version" +require_relative "custom.gemspec" + +# NOTE: A handful of these fields are required as part of the Ruby specification. +# You can change them here or overwrite them in the custom gemspec file. +Gem::Specification.new do |spec| + spec.name = "fern_basic-auth-optional" + spec.authors = ["Seed"] + spec.version = Seed::VERSION + spec.summary = "Ruby client library for the Seed API" + spec.description = "The Seed Ruby library provides convenient access to the Seed API from Ruby." + spec.required_ruby_version = ">= 3.3.0" + spec.metadata["rubygems_mfa_required"] = "true" + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) + end + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "base64" + # For more information and examples about making a new gem, check out our + # guide at: https://bundler.io/guides/creating_gem.html + + # Load custom gemspec configuration if it exists + custom_gemspec_file = File.join(__dir__, "custom.gemspec.rb") + add_custom_gemspec_data(spec) if File.exist?(custom_gemspec_file) +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/snippet.json b/seed/ruby-sdk-v2/basic-auth-optional/snippet.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb new file mode 100644 index 000000000000..4bd57989d43d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# This is a custom test file, if you wish to add more tests +# to your SDK. +# Be sure to mark this file in `.fernignore`. +# +# If you include example requests/responses in your fern definition, +# you will have tests automatically generated for you. + +# This test is run via command line: rake customtest +describe "Custom Test" do + it "Default" do + refute false + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb new file mode 100644 index 000000000000..b086fe6d76ec --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "../lib/seed" diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb new file mode 100644 index 000000000000..44f85cb20b35 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "stringio" +require "json" +require "test_helper" + +NUMBERS = (1..65).to_a +PageResponse = Struct.new(:cards, :next_cursor) + +class CursorItemIteratorTest < Minitest::Test + def make_iterator(initial_cursor:) + @times_called = 0 + + Seed::Internal::CursorItemIterator.new(initial_cursor:, cursor_field: :next_cursor, item_field: :cards) do |cursor| + @times_called += 1 + cursor ||= 0 + next_cursor = cursor + 10 + PageResponse.new( + cards: NUMBERS[cursor...next_cursor], + next_cursor: next_cursor < NUMBERS.length ? next_cursor : nil + ) + end + end + + def test_item_iterator_can_iterate_to_exhaustion + iterator = make_iterator(initial_cursor: 0) + + assert_equal NUMBERS, iterator.to_a + assert_equal 7, @times_called + + iterator = make_iterator(initial_cursor: 10) + + assert_equal (11..65).to_a, iterator.to_a + + iterator = make_iterator(initial_cursor: 5) + + assert_equal (6..65).to_a, iterator.to_a + end + + def test_item_iterator_can_work_without_an_initial_cursor + iterator = make_iterator(initial_cursor: nil) + + assert_equal NUMBERS, iterator.to_a + assert_equal 7, @times_called + end + + def test_items_iterator_iterates_lazily + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + assert_equal 1, iterator.first + assert_equal 1, @times_called + + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + assert_equal (1..15).to_a, iterator.first(15) + assert_equal 2, @times_called + + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + iterator.each do |card| + break if card >= 15 + end + + assert_equal 2, @times_called + end + + def test_items_iterator_implements_enumerable + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + doubled = iterator.map { |card| card * 2 } + + assert_equal 7, @times_called + assert_equal NUMBERS.length, doubled.length + end + + def test_items_iterator_can_be_advanced_manually + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + + items = [] + expected_times_called = 0 + while (item = iterator.next_element) + expected_times_called += 1 if (item % 10) == 1 + + assert_equal expected_times_called, @times_called + assert_equal item != NUMBERS.last, iterator.next?, "#{item} #{iterator}" + items.push(item) + end + + assert_equal 7, @times_called + assert_equal NUMBERS, items + end + + def test_pages_iterator + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal( + [ + (1..10).to_a, + (11..20).to_a, + (21..30).to_a, + (31..40).to_a, + (41..50).to_a, + (51..60).to_a, + (61..65).to_a + ], + iterator.to_a.map(&:cards) + ) + + iterator = make_iterator(initial_cursor: 10).pages + + assert_equal( + [ + (11..20).to_a, + (21..30).to_a, + (31..40).to_a, + (41..50).to_a, + (51..60).to_a, + (61..65).to_a + ], + iterator.to_a.map(&:cards) + ) + end + + def test_pages_iterator_can_work_without_an_initial_cursor + iterator = make_iterator(initial_cursor: nil).pages + + assert_equal 7, iterator.to_a.length + assert_equal 7, @times_called + end + + def test_pages_iterator_iterates_lazily + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + iterator.first + + assert_equal 1, @times_called + + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + assert_equal 2, iterator.first(2).length + assert_equal 2, @times_called + end + + def test_pages_iterator_knows_whether_another_page_is_upcoming + iterator = make_iterator(initial_cursor: 0).pages + + iterator.each_with_index do |_page, index| + assert_equal index + 1, @times_called + assert_equal index < 6, iterator.next? + end + end + + def test_pages_iterator_can_be_advanced_manually + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + + lengths = [] + expected_times_called = 0 + while (page = iterator.next_page) + expected_times_called += 1 + + assert_equal expected_times_called, @times_called + lengths.push(page.cards.length) + end + + assert_equal 7, @times_called + assert_equal [10, 10, 10, 10, 10, 10, 5], lengths + end + + def test_pages_iterator_implements_enumerable + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + lengths = iterator.map { |page| page.cards.length } + + assert_equal 7, @times_called + assert_equal [10, 10, 10, 10, 10, 10, 5], lengths + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb new file mode 100644 index 000000000000..004f394f0a41 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "stringio" +require "json" +require "test_helper" + +OffsetPageResponse = Struct.new(:items, :has_next) +TestIteratorConfig = Struct.new( + :step, + :has_next_field, + :total_item_count, + :per_page, + :initial_page +) do + def first_item_returned + if step + (initial_page || 0) + 1 + else + (((initial_page || 1) - 1) * per_page) + 1 + end + end +end + +LAZY_TEST_ITERATOR_CONFIG = TestIteratorConfig.new(initial_page: 1, step: false, has_next_field: :has_next, total_item_count: 65, per_page: 10) +ALL_TEST_ITERATOR_CONFIGS = [true, false].map do |step| + [:has_next, nil].map do |has_next_field| + [0, 5, 10, 60, 63].map do |total_item_count| + [5, 10].map do |per_page| + initial_pages = [nil, 3, 100] + initial_pages << (step ? 0 : 1) + + initial_pages.map do |initial_page| + TestIteratorConfig.new( + step: step, + has_next_field: has_next_field, + total_item_count: total_item_count, + per_page: per_page, + initial_page: initial_page + ) + end + end + end + end +end.flatten + +class OffsetItemIteratorTest < Minitest::Test + def make_iterator(config) + @times_called = 0 + + items = (1..config.total_item_count).to_a + + Seed::Internal::OffsetItemIterator.new( + initial_page: config.initial_page, + item_field: :items, + has_next_field: config.has_next_field, + step: config.step + ) do |page| + @times_called += 1 + + slice_start = config.step ? page : (page - 1) * config.per_page + slice_end = slice_start + config.per_page + + output = { + items: items[slice_start...slice_end] + } + output[config.has_next_field] = slice_end < items.length if config.has_next_field + + OffsetPageResponse.new(**output) + end + end + + def test_item_iterator_can_iterate_to_exhaustion + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config) + + assert_equal (config.first_item_returned..config.total_item_count).to_a, iterator.to_a + end + end + + def test_items_iterator_can_be_advanced_manually_and_has_accurate_has_next + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config) + items = [] + + while (item = iterator.next_element) + assert_equal(item != config.total_item_count, iterator.next?, "#{item} #{iterator}") + items.push(item) + end + + assert_equal (config.first_item_returned..config.total_item_count).to_a, items + end + end + + def test_pages_iterator_can_be_advanced_manually_and_has_accurate_has_next + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config).pages + pages = [] + + loop do + has_next_output = iterator.next? + page = iterator.next_page + + assert_equal(has_next_output, !page.nil?, "next? was inaccurate: #{config} #{iterator.inspect}") + break if page.nil? + + pages.push(page) + end + + assert_equal pages, make_iterator(config).pages.to_a + end + end + + def test_items_iterator_iterates_lazily + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + assert_equal 1, iterator.first + assert_equal 1, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + assert_equal (1..15).to_a, iterator.first(15) + assert_equal 2, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + iterator.each do |card| + break if card >= 15 + end + + assert_equal 2, @times_called + end + + def test_pages_iterator_iterates_lazily + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG).pages + + assert_equal 0, @times_called + iterator.first + + assert_equal 1, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG).pages + + assert_equal 0, @times_called + assert_equal 3, iterator.first(3).length + assert_equal 3, @times_called + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb new file mode 100644 index 000000000000..e7e6571f03ee --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Array do + module TestArray + StringArray = Seed::Internal::Types::Array[String] + end + + describe "#initialize" do + it "sets the type" do + assert_equal String, TestArray::StringArray.type + end + end + + describe "#coerce" do + it "does not perform coercion if not an array" do + assert_equal 1, TestArray::StringArray.coerce(1) + end + + it "raises an error if not an array and strictness is on" do + assert_raises Seed::Internal::Errors::TypeError do + TestArray::StringArray.coerce(1, strict: true) + end + end + + it "coerces the elements" do + assert_equal %w[foobar 1 true], TestArray::StringArray.coerce(["foobar", 1, true]) + end + + it "raises an error if element of array is not coercable and strictness is on" do + assert_raises Seed::Internal::Errors::TypeError do + TestArray::StringArray.coerce([Object.new], strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb new file mode 100644 index 000000000000..cba18e48765b --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Boolean do + describe ".coerce" do + it "coerces true/false" do + assert Seed::Internal::Types::Boolean.coerce(true) + refute Seed::Internal::Types::Boolean.coerce(false) + end + + it "coerces an Integer" do + assert Seed::Internal::Types::Boolean.coerce(1) + refute Seed::Internal::Types::Boolean.coerce(0) + end + + it "coerces a String" do + assert Seed::Internal::Types::Boolean.coerce("1") + assert Seed::Internal::Types::Boolean.coerce("true") + refute Seed::Internal::Types::Boolean.coerce("0") + end + + it "passes through other values with strictness off" do + obj = Object.new + + assert_equal obj, Seed::Internal::Types::Boolean.coerce(obj) + end + + it "raises an error with other values with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + Seed::Internal::Types::Boolean.coerce(Object.new, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb new file mode 100644 index 000000000000..e8d89bce467f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Enum do + module EnumTest + module ExampleEnum + extend Seed::Internal::Types::Enum + + FOO = :foo + BAR = :bar + + finalize! + end + end + + describe "#values" do + it "defines values" do + assert_equal %i[foo bar].sort, EnumTest::ExampleEnum.values.sort + end + end + + describe "#coerce" do + it "coerces an existing member" do + assert_equal :foo, EnumTest::ExampleEnum.coerce(:foo) + end + + it "coerces a string version of a member" do + assert_equal :foo, EnumTest::ExampleEnum.coerce("foo") + end + + it "returns the value if not a member with strictness off" do + assert_equal 1, EnumTest::ExampleEnum.coerce(1) + end + + it "raises an error if value is not a member with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + EnumTest::ExampleEnum.coerce(1, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb new file mode 100644 index 000000000000..6c5e58a6a946 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Hash do + module TestHash + SymbolStringHash = Seed::Internal::Types::Hash[Symbol, String] + end + + describe ".[]" do + it "defines the key and value type" do + assert_equal Symbol, TestHash::SymbolStringHash.key_type + assert_equal String, TestHash::SymbolStringHash.value_type + end + end + + describe "#coerce" do + it "coerces the keys" do + assert_equal %i[foo bar], TestHash::SymbolStringHash.coerce({ "foo" => "1", :bar => "2" }).keys + end + + it "coerces the values" do + assert_equal %w[foo 1], TestHash::SymbolStringHash.coerce({ foo: :foo, bar: 1 }).values + end + + it "passes through other values with strictness off" do + obj = Object.new + + assert_equal obj, TestHash::SymbolStringHash.coerce(obj) + end + + it "raises an error with other values with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce(Object.new, strict: true) + end + end + + it "raises an error with non-coercable key types with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce({ Object.new => 1 }, strict: true) + end + end + + it "raises an error with non-coercable value types with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce({ "foobar" => Object.new }, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb new file mode 100644 index 000000000000..3d87b9f5a8c7 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Model do + module StringInteger + extend Seed::Internal::Types::Union + + member String + member Integer + end + + class ExampleModel < Seed::Internal::Types::Model + field :name, String + field :rating, StringInteger, optional: true + field :year, Integer, optional: true, nullable: true, api_name: "yearOfRelease" + end + + class ExampleModelInheritance < ExampleModel + field :director, String + end + + class ExampleWithDefaults < ExampleModel + field :type, String, default: "example" + end + + class ExampleChild < Seed::Internal::Types::Model + field :value, String + end + + class ExampleParent < Seed::Internal::Types::Model + field :child, ExampleChild + end + + describe ".field" do + before do + @example = ExampleModel.new(name: "Inception", rating: 4) + end + + it "defines fields on model" do + assert_equal %i[name rating year], ExampleModel.fields.keys + end + + it "defines fields from parent models" do + assert_equal %i[name rating year director], ExampleModelInheritance.fields.keys + end + + it "sets the field's type" do + assert_equal String, ExampleModel.fields[:name].type + assert_equal StringInteger, ExampleModel.fields[:rating].type + end + + it "sets the `default` option" do + assert_equal "example", ExampleWithDefaults.fields[:type].default + end + + it "defines getters" do + assert_respond_to @example, :name + assert_respond_to @example, :rating + + assert_equal "Inception", @example.name + assert_equal 4, @example.rating + end + + it "defines setters" do + assert_respond_to @example, :name= + assert_respond_to @example, :rating= + + @example.name = "Inception 2" + @example.rating = 5 + + assert_equal "Inception 2", @example.name + assert_equal 5, @example.rating + end + end + + describe "#initialize" do + it "sets the data" do + example = ExampleModel.new(name: "Inception", rating: 4) + + assert_equal "Inception", example.name + assert_equal 4, example.rating + end + + it "allows extra fields to be set" do + example = ExampleModel.new(name: "Inception", rating: 4, director: "Christopher Nolan") + + assert_equal "Christopher Nolan", example.director + end + + it "sets the defaults where applicable" do + example_using_defaults = ExampleWithDefaults.new + + assert_equal "example", example_using_defaults.type + + example_without_defaults = ExampleWithDefaults.new(type: "not example") + + assert_equal "not example", example_without_defaults.type + end + + it "coerces child models" do + parent = ExampleParent.new(child: { value: "foobar" }) + + assert_kind_of ExampleChild, parent.child + end + + it "uses the api_name to pull the value" do + example = ExampleModel.new({ name: "Inception", yearOfRelease: 2014 }) + + assert_equal 2014, example.year + refute_respond_to example, :yearOfRelease + end + end + + describe "#inspect" do + class SensitiveModel < Seed::Internal::Types::Model + field :username, String + field :password, String + field :client_secret, String + field :access_token, String + field :api_key, String + end + + it "redacts sensitive fields" do + model = SensitiveModel.new( + username: "user123", + password: "secret123", + client_secret: "cs_abc", + access_token: "token_xyz", + api_key: "key_123" + ) + + inspect_output = model.inspect + + assert_includes inspect_output, "username=\"user123\"" + assert_includes inspect_output, "password=[REDACTED]" + assert_includes inspect_output, "client_secret=[REDACTED]" + assert_includes inspect_output, "access_token=[REDACTED]" + assert_includes inspect_output, "api_key=[REDACTED]" + refute_includes inspect_output, "secret123" + refute_includes inspect_output, "cs_abc" + refute_includes inspect_output, "token_xyz" + refute_includes inspect_output, "key_123" + end + + it "does not redact non-sensitive fields" do + example = ExampleModel.new(name: "Inception", rating: 4) + inspect_output = example.inspect + + assert_includes inspect_output, "name=\"Inception\"" + assert_includes inspect_output, "rating=4" + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb new file mode 100644 index 000000000000..e4e95c93139f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Union do + class Rectangle < Seed::Internal::Types::Model + literal :type, "square" + + field :area, Float + end + + class Circle < Seed::Internal::Types::Model + literal :type, "circle" + + field :area, Float + end + + class Pineapple < Seed::Internal::Types::Model + literal :type, "pineapple" + + field :area, Float + end + + module Shape + extend Seed::Internal::Types::Union + + discriminant :type + + member -> { Rectangle }, key: "rect" + member -> { Circle }, key: "circle" + end + + module StringOrInteger + extend Seed::Internal::Types::Union + + member String + member Integer + end + + describe "#coerce" do + it "coerces hashes into member models with discriminated unions" do + circle = Shape.coerce({ type: "circle", area: 4.0 }) + + assert_instance_of Circle, circle + end + end + + describe "#type_member?" do + it "defines Model members" do + assert Shape.type_member?(Rectangle) + assert Shape.type_member?(Circle) + refute Shape.type_member?(Pineapple) + end + + it "defines other members" do + assert StringOrInteger.type_member?(String) + assert StringOrInteger.type_member?(Integer) + refute StringOrInteger.type_member?(Float) + refute StringOrInteger.type_member?(Pineapple) + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb new file mode 100644 index 000000000000..29d14621a229 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Utils do + Utils = Seed::Internal::Types::Utils + + module TestUtils + class M < Seed::Internal::Types::Model + field :value, String + end + + class UnionMemberA < Seed::Internal::Types::Model + literal :type, "A" + field :only_on_a, String + end + + class UnionMemberB < Seed::Internal::Types::Model + literal :type, "B" + field :only_on_b, String + end + + module U + extend Seed::Internal::Types::Union + + discriminant :type + + member -> { UnionMemberA }, key: "A" + member -> { UnionMemberB }, key: "B" + end + + SymbolStringHash = Seed::Internal::Types::Hash[Symbol, String] + SymbolModelHash = -> { Seed::Internal::Types::Hash[Symbol, TestUtils::M] } + end + + describe ".coerce" do + describe "NilClass" do + it "always returns nil" do + assert_nil Utils.coerce(NilClass, "foobar") + assert_nil Utils.coerce(NilClass, 1) + assert_nil Utils.coerce(NilClass, Object.new) + end + end + + describe "String" do + it "coerces from String, Symbol, Numeric, or Boolean" do + assert_equal "foobar", Utils.coerce(String, "foobar") + assert_equal "foobar", Utils.coerce(String, :foobar) + assert_equal "1", Utils.coerce(String, 1) + assert_equal "1.0", Utils.coerce(String, 1.0) + assert_equal "true", Utils.coerce(String, true) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(String, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(String, Object.new, strict: true) + end + end + end + + describe "Symbol" do + it "coerces from Symbol, String" do + assert_equal :foobar, Utils.coerce(Symbol, :foobar) + assert_equal :foobar, Utils.coerce(Symbol, "foobar") + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Symbol, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Symbol, Object.new, strict: true) + end + end + end + + describe "Integer" do + it "coerces from Numeric, String, Time" do + assert_equal 1, Utils.coerce(Integer, 1) + assert_equal 1, Utils.coerce(Integer, 1.0) + assert_equal 1, Utils.coerce(Integer, Complex.rect(1)) + assert_equal 1, Utils.coerce(Integer, Rational(1)) + assert_equal 1, Utils.coerce(Integer, "1") + assert_equal 1_713_916_800, Utils.coerce(Integer, Time.utc(2024, 4, 24)) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Integer, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Integer, Object.new, strict: true) + end + end + end + + describe "Float" do + it "coerces from Numeric, Time" do + assert_in_delta(1.0, Utils.coerce(Float, 1.0)) + assert_in_delta(1.0, Utils.coerce(Float, 1)) + assert_in_delta(1.0, Utils.coerce(Float, Complex.rect(1))) + assert_in_delta(1.0, Utils.coerce(Float, Rational(1))) + assert_in_delta(1_713_916_800.0, Utils.coerce(Integer, Time.utc(2024, 4, 24))) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Float, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Float, Object.new, strict: true) + end + end + end + + describe "Model" do + it "coerces a hash" do + result = Utils.coerce(TestUtils::M, { value: "foobar" }) + + assert_kind_of TestUtils::M, result + assert_equal "foobar", result.value + end + + it "coerces a hash when the target is a type function" do + result = Utils.coerce(-> { TestUtils::M }, { value: "foobar" }) + + assert_kind_of TestUtils::M, result + assert_equal "foobar", result.value + end + + it "will not coerce non-hashes" do + assert_equal "foobar", Utils.coerce(TestUtils::M, "foobar") + end + end + + describe "Enum" do + module ExampleEnum + extend Seed::Internal::Types::Enum + + FOO = :FOO + BAR = :BAR + + finalize! + end + + it "coerces into a Symbol version of the member value" do + assert_equal :FOO, Utils.coerce(ExampleEnum, "FOO") + end + + it "returns given value if not a member" do + assert_equal "NOPE", Utils.coerce(ExampleEnum, "NOPE") + end + end + + describe "Array" do + StringArray = Seed::Internal::Types::Array[String] + ModelArray = -> { Seed::Internal::Types::Array[TestUtils::M] } + UnionArray = -> { Seed::Internal::Types::Array[TestUtils::U] } + + it "coerces an array of literals" do + assert_equal %w[a b c], Utils.coerce(StringArray, %w[a b c]) + assert_equal ["1", "2.0", "true"], Utils.coerce(StringArray, [1, 2.0, true]) + assert_equal ["1", "2.0", "true"], Utils.coerce(StringArray, Set.new([1, 2.0, true])) + end + + it "coerces an array of Models" do + assert_equal [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")], + Utils.coerce(ModelArray, [{ value: "foobar" }, { value: "bizbaz" }]) + + assert_equal [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")], + Utils.coerce(ModelArray, [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")]) + end + + it "coerces an array of model unions" do + assert_equal [TestUtils::UnionMemberA.new(type: "A", only_on_a: "A"), TestUtils::UnionMemberB.new(type: "B", only_on_b: "B")], + Utils.coerce(UnionArray, [{ type: "A", only_on_a: "A" }, { type: "B", only_on_b: "B" }]) + end + + it "returns given value if not an array" do + assert_equal 1, Utils.coerce(StringArray, 1) + end + end + + describe "Hash" do + it "coerces the keys and values" do + ssh_res = Utils.coerce(TestUtils::SymbolStringHash, { "foo" => "bar", "biz" => "2" }) + + assert_equal "bar", ssh_res[:foo] + assert_equal "2", ssh_res[:biz] + + smh_res = Utils.coerce(TestUtils::SymbolModelHash, { "foo" => { "value" => "foo" } }) + + assert_equal TestUtils::M.new(value: "foo"), smh_res[:foo] + end + end + end +end 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 0a456d4cbab9cb42480791d2cf1b18f80696f0fd Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:53:15 +0000 Subject: [PATCH 02/20] fix(ruby-sdk): use per-field omit checks and constructor optionality instead of coarse eitherOmitted flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index d7f5938012ab..0db152d557e6 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -124,15 +124,27 @@ export class RootClientGenerator extends FileGenerator; - const eitherOmitted = scheme.usernameOmit === true || scheme.passwordOmit === true; - const conditionOp = eitherOmitted ? "||" : "&&"; - const usernameExpr = eitherOmitted ? `${usernameName} || ""` : usernameName; - const passwordExpr = eitherOmitted ? `${passwordName} || ""` : passwordName; + const usernameOmitted = scheme.usernameOmit === true; + const passwordOmitted = scheme.passwordOmit === true; + // Per-field null coalescing: only omittable fields get || "" fallback + const usernameExpr = usernameOmitted ? `${usernameName} || ""` : usernameName; + const passwordExpr = passwordOmitted ? `${passwordName} || ""` : passwordName; + // Per-field condition: required fields must be present, omittable fields are always satisfied + let condition: string; + if (!usernameOmitted && !passwordOmitted) { + condition = `!${usernameName}.nil? && !${passwordName}.nil?`; + } else if (usernameOmitted && passwordOmitted) { + condition = `!${usernameName}.nil? || !${passwordName}.nil?`; + } else if (usernameOmitted) { + condition = `!${passwordName}.nil?`; + } else { + condition = `!${usernameName}.nil?`; + } if (isAuthOptional || basicAuthSchemes.length > 1) { if (i === 0) { - writer.writeLine(`if !${usernameName}.nil? ${conditionOp} !${passwordName}.nil?`); + writer.writeLine(`if ${condition}`); } else { - writer.writeLine(`elsif !${usernameName}.nil? ${conditionOp} !${passwordName}.nil?`); + writer.writeLine(`elsif ${condition}`); } writer.writeLine( ` headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameExpr}}:#{${passwordExpr}}")}"` @@ -348,27 +360,37 @@ export class RootClientGenerator extends FileGenerator; + const usernameOmitted = schemeRecord.usernameOmit === true; + const passwordOmitted = schemeRecord.passwordOmit === true; + const usernameType = usernameOmitted ? ruby.Type.nilable(ruby.Type.string()) : ruby.Type.string(); + const passwordType = passwordOmitted ? ruby.Type.nilable(ruby.Type.string()) : ruby.Type.string(); const usernameParam = ruby.parameters.keyword({ name: scheme.username.snakeCase.safeName, - type: ruby.Type.string(), + type: usernameType, initializer: - scheme.usernameEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); - }) - : undefined, + usernameOmitted + ? ruby.nilValue() + : scheme.usernameEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); + }) + : undefined, docs: undefined }); parameters.push(usernameParam); const passwordParam = ruby.parameters.keyword({ name: scheme.password.snakeCase.safeName, - type: ruby.Type.string(), + type: passwordType, initializer: - scheme.passwordEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); - }) - : undefined, + passwordOmitted + ? ruby.nilValue() + : scheme.passwordEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); + }) + : undefined, docs: undefined }); parameters.push(passwordParam); From 152f41371a6c91a2b0d3b3046237b274ade3a6cb Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:59:48 +0000 Subject: [PATCH 03/20] fix(ruby-sdk): fix biome formatting for ternary expressions in constructor params Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 0db152d557e6..33c0b1b27aca 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -369,28 +369,26 @@ export class RootClientGenerator extends FileGenerator { - writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); - }) - : undefined, + initializer: usernameOmitted + ? ruby.nilValue() + : scheme.usernameEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); + }) + : undefined, docs: undefined }); parameters.push(usernameParam); const passwordParam = ruby.parameters.keyword({ name: scheme.password.snakeCase.safeName, type: passwordType, - initializer: - passwordOmitted - ? ruby.nilValue() - : scheme.passwordEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); - }) - : undefined, + initializer: passwordOmitted + ? ruby.nilValue() + : scheme.passwordEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); + }) + : undefined, docs: undefined }); parameters.push(passwordParam); From 911da894f003426f115b0de8119e307d2ee3e46e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:17:20 +0000 Subject: [PATCH 04/20] fix(ruby-sdk): remove omitted fields entirely from constructor params, use empty string internally Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/root-client/RootClientGenerator.ts | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 33c0b1b27aca..0ef53baeda5c 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -126,19 +126,19 @@ export class RootClientGenerator extends FileGenerator; const usernameOmitted = scheme.usernameOmit === true; const passwordOmitted = scheme.passwordOmit === true; - // Per-field null coalescing: only omittable fields get || "" fallback - const usernameExpr = usernameOmitted ? `${usernameName} || ""` : usernameName; - const passwordExpr = passwordOmitted ? `${passwordName} || ""` : passwordName; - // Per-field condition: required fields must be present, omittable fields are always satisfied + // Omitted fields use empty string directly + const usernameExpr = usernameOmitted ? `""` : usernameName; + const passwordExpr = passwordOmitted ? `""` : passwordName; + // Condition: only require non-omitted fields to be present let condition: string; if (!usernameOmitted && !passwordOmitted) { condition = `!${usernameName}.nil? && !${passwordName}.nil?`; - } else if (usernameOmitted && passwordOmitted) { - condition = `!${usernameName}.nil? || !${passwordName}.nil?`; - } else if (usernameOmitted) { + } else if (usernameOmitted && !passwordOmitted) { condition = `!${passwordName}.nil?`; - } else { + } else if (!usernameOmitted && passwordOmitted) { condition = `!${usernameName}.nil?`; + } else { + condition = `true`; } if (isAuthOptional || basicAuthSchemes.length > 1) { if (i === 0) { @@ -360,38 +360,38 @@ export class RootClientGenerator extends FileGenerator; const usernameOmitted = schemeRecord.usernameOmit === true; const passwordOmitted = schemeRecord.passwordOmit === true; - const usernameType = usernameOmitted ? ruby.Type.nilable(ruby.Type.string()) : ruby.Type.string(); - const passwordType = passwordOmitted ? ruby.Type.nilable(ruby.Type.string()) : ruby.Type.string(); - const usernameParam = ruby.parameters.keyword({ - name: scheme.username.snakeCase.safeName, - type: usernameType, - initializer: usernameOmitted - ? ruby.nilValue() - : scheme.usernameEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); - }) - : undefined, - docs: undefined - }); - parameters.push(usernameParam); - const passwordParam = ruby.parameters.keyword({ - name: scheme.password.snakeCase.safeName, - type: passwordType, - initializer: passwordOmitted - ? ruby.nilValue() - : scheme.passwordEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); - }) - : undefined, - docs: undefined - }); - parameters.push(passwordParam); + if (!usernameOmitted) { + const usernameParam = ruby.parameters.keyword({ + name: scheme.username.snakeCase.safeName, + type: ruby.Type.string(), + initializer: + scheme.usernameEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); + }) + : undefined, + docs: undefined + }); + parameters.push(usernameParam); + } + if (!passwordOmitted) { + const passwordParam = ruby.parameters.keyword({ + name: scheme.password.snakeCase.safeName, + type: ruby.Type.string(), + initializer: + scheme.passwordEnvVar != null + ? ruby.codeblock((writer) => { + writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); + }) + : undefined, + docs: undefined + }); + parameters.push(passwordParam); + } break; } case "inferred": { From 05c7d3e9ca08334b04752c3e5e2c3169c22bd31b Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:24:21 +0000 Subject: [PATCH 05/20] fix(ruby-sdk): skip auth header when both fields omitted and auth is non-mandatory Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 0ef53baeda5c..36be6824d2e5 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -138,7 +138,8 @@ export class RootClientGenerator extends FileGenerator 1) { if (i === 0) { From fcb906f78495db09d36771543abf25e7afc26b18 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:33:30 +0000 Subject: [PATCH 06/20] fix(ruby-sdk): use isFirstBlock to prevent else if without preceding if when first scheme skipped Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/root-client/RootClientGenerator.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 36be6824d2e5..50636535111c 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -115,6 +115,8 @@ export class RootClientGenerator extends FileGenerator 1) { - if (i === 0) { + if (isFirstBlock) { writer.writeLine(`if ${condition}`); } else { writer.writeLine(`elsif ${condition}`); } + isFirstBlock = false; + emittedAnyBlock = true; writer.writeLine( ` headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameExpr}}:#{${passwordExpr}}")}"` ); - if (i === basicAuthSchemes.length - 1) { - writer.writeLine(`end`); - } } else { writer.writeLine( `headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameExpr}}:#{${passwordExpr}}")}"` ); } } + if (emittedAnyBlock && (isAuthOptional || basicAuthSchemes.length > 1)) { + writer.writeLine(`end`); + } } writer.write(`@raw_client = `); writer.writeNode(this.context.getRawClientClassReference()); From bf8d60b438ed8ab8a5b07bdbb876dbc6e893c6e7 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:19:25 +0000 Subject: [PATCH 07/20] fix(ruby-sdk): use 'omit' instead of 'optional' in versions.yml changelog and code comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ruby-v2/sdk/src/root-client/RootClientGenerator.ts | 2 +- generators/ruby-v2/sdk/versions.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 50636535111c..161e0815869f 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -140,7 +140,7 @@ export class RootClientGenerator extends FileGenerator 1) { diff --git a/generators/ruby-v2/sdk/versions.yml b/generators/ruby-v2/sdk/versions.yml index 24fef32c59c3..7c1c3ff78f2a 100644 --- a/generators/ruby-v2/sdk/versions.yml +++ b/generators/ruby-v2/sdk/versions.yml @@ -3,11 +3,11 @@ - version: 1.1.14 changelogEntry: - summary: | - Support optional username and password in basic auth. The SDK now accepts - username-only, password-only, or both credentials. Missing fields are treated - as empty strings (e.g., username-only encodes `username:`, password-only - encodes `:password`). When neither is provided, the Authorization header is - omitted entirely. + Support omitting username or password from basic auth when configured via + `usernameOmit` or `passwordOmit` in the IR. Omitted fields are removed from + the SDK's public API and treated as empty strings internally (e.g., omitting + password encodes `username:`, omitting username encodes `:password`). When + both are omitted, the Authorization header is skipped entirely. type: feat createdAt: "2026-04-02" irVersion: 61 From 1eaea0e3a3e70e881e3d025878bb6f7a3b02dcae Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:40:23 +0000 Subject: [PATCH 08/20] 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> --- .../type_errors_UnauthorizedRequestErrorBody.json | 0 .../.fern/metadata.json | 0 .../.github/workflows/ci.yml | 0 .../{basic-auth-optional => basic-auth-pw-omitted}/.gitignore | 0 .../.rubocop.yml | 0 .../{basic-auth-optional => basic-auth-pw-omitted}/Gemfile | 0 .../Gemfile.custom | 0 .../{basic-auth-optional => basic-auth-pw-omitted}/README.md | 0 .../{basic-auth-optional => basic-auth-pw-omitted}/Rakefile | 0 .../custom.gemspec.rb | 0 .../dynamic-snippets/example0/snippet.rb | 0 .../dynamic-snippets/example1/snippet.rb | 0 .../dynamic-snippets/example2/snippet.rb | 0 .../dynamic-snippets/example3/snippet.rb | 0 .../dynamic-snippets/example4/snippet.rb | 0 .../dynamic-snippets/example5/snippet.rb | 0 .../dynamic-snippets/example6/snippet.rb | 0 .../lib/seed.rb | 0 .../lib/seed/basic_auth/client.rb | 0 .../lib/seed/client.rb | 2 +- .../lib/seed/errors/api_error.rb | 0 .../lib/seed/errors/client_error.rb | 0 .../lib/seed/errors/redirect_error.rb | 0 .../lib/seed/errors/response_error.rb | 0 .../lib/seed/errors/server_error.rb | 0 .../lib/seed/errors/timeout_error.rb | 0 .../lib/seed/errors/types/unauthorized_request_error_body.rb | 0 .../lib/seed/internal/errors/constraint_error.rb | 0 .../lib/seed/internal/errors/type_error.rb | 0 .../lib/seed/internal/http/base_request.rb | 0 .../lib/seed/internal/http/raw_client.rb | 0 .../lib/seed/internal/iterators/cursor_item_iterator.rb | 0 .../lib/seed/internal/iterators/cursor_page_iterator.rb | 0 .../lib/seed/internal/iterators/item_iterator.rb | 0 .../lib/seed/internal/iterators/offset_item_iterator.rb | 0 .../lib/seed/internal/iterators/offset_page_iterator.rb | 0 .../lib/seed/internal/json/request.rb | 0 .../lib/seed/internal/json/serializable.rb | 0 .../lib/seed/internal/multipart/multipart_encoder.rb | 0 .../lib/seed/internal/multipart/multipart_form_data.rb | 0 .../lib/seed/internal/multipart/multipart_form_data_part.rb | 0 .../lib/seed/internal/multipart/multipart_request.rb | 0 .../lib/seed/internal/types/array.rb | 0 .../lib/seed/internal/types/boolean.rb | 0 .../lib/seed/internal/types/enum.rb | 0 .../lib/seed/internal/types/hash.rb | 0 .../lib/seed/internal/types/model.rb | 0 .../lib/seed/internal/types/model/field.rb | 0 .../lib/seed/internal/types/type.rb | 0 .../lib/seed/internal/types/union.rb | 0 .../lib/seed/internal/types/unknown.rb | 0 .../lib/seed/internal/types/utils.rb | 0 .../lib/seed/version.rb | 0 .../reference.md | 0 .../seed.gemspec | 2 +- .../snippet.json | 0 .../test/custom.test.rb | 0 .../test/test_helper.rb | 0 .../test/unit/internal/iterators/test_cursor_item_iterator.rb | 0 .../test/unit/internal/iterators/test_offset_item_iterator.rb | 0 .../test/unit/internal/types/test_array.rb | 0 .../test/unit/internal/types/test_boolean.rb | 0 .../test/unit/internal/types/test_enum.rb | 0 .../test/unit/internal/types/test_hash.rb | 0 .../test/unit/internal/types/test_model.rb | 0 .../test/unit/internal/types/test_union.rb | 0 .../test/unit/internal/types/test_utils.rb | 0 .../definition/api.yml | 2 +- .../definition/basic-auth.yml | 0 .../definition/errors.yml | 0 .../generators.yml | 4 ++-- 71 files changed, 5 insertions(+), 5 deletions(-) rename packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/{basic-auth-optional => basic-auth-pw-omitted}/type_errors_UnauthorizedRequestErrorBody.json (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/.fern/metadata.json (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/.github/workflows/ci.yml (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/.gitignore (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/.rubocop.yml (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/Gemfile (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/Gemfile.custom (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/README.md (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/Rakefile (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/custom.gemspec.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example0/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example1/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example2/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example3/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example4/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example5/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/dynamic-snippets/example6/snippet.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/basic_auth/client.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/client.rb (91%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/api_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/client_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/redirect_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/response_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/server_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/timeout_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/errors/types/unauthorized_request_error_body.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/errors/constraint_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/errors/type_error.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/http/base_request.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/http/raw_client.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/iterators/cursor_item_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/iterators/cursor_page_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/iterators/item_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/iterators/offset_item_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/iterators/offset_page_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/json/request.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/json/serializable.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/multipart/multipart_encoder.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/multipart/multipart_form_data.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/multipart/multipart_form_data_part.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/multipart/multipart_request.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/array.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/boolean.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/enum.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/hash.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/model.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/model/field.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/type.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/union.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/unknown.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/internal/types/utils.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/lib/seed/version.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/reference.md (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/seed.gemspec (97%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/snippet.json (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/custom.test.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/test_helper.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/iterators/test_cursor_item_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/iterators/test_offset_item_iterator.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_array.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_boolean.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_enum.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_hash.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_model.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_union.rb (100%) rename seed/ruby-sdk-v2/{basic-auth-optional => basic-auth-pw-omitted}/test/unit/internal/types/test_utils.rb (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/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/.fern/metadata.json similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/.fern/metadata.json rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/.fern/metadata.json diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml b/seed/ruby-sdk-v2/basic-auth-pw-omitted/.github/workflows/ci.yml similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/.github/workflows/ci.yml rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/.github/workflows/ci.yml diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.gitignore b/seed/ruby-sdk-v2/basic-auth-pw-omitted/.gitignore similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/.gitignore rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/.gitignore diff --git a/seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml b/seed/ruby-sdk-v2/basic-auth-pw-omitted/.rubocop.yml similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/.rubocop.yml rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/.rubocop.yml diff --git a/seed/ruby-sdk-v2/basic-auth-optional/Gemfile b/seed/ruby-sdk-v2/basic-auth-pw-omitted/Gemfile similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/Gemfile rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/Gemfile diff --git a/seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom b/seed/ruby-sdk-v2/basic-auth-pw-omitted/Gemfile.custom similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/Gemfile.custom rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/Gemfile.custom diff --git a/seed/ruby-sdk-v2/basic-auth-optional/README.md b/seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/README.md rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md diff --git a/seed/ruby-sdk-v2/basic-auth-optional/Rakefile b/seed/ruby-sdk-v2/basic-auth-pw-omitted/Rakefile similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/Rakefile rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/Rakefile diff --git a/seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/custom.gemspec.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/custom.gemspec.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/custom.gemspec.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example0/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example1/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example2/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example3/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example4/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example5/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/dynamic-snippets/example6/snippet.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/basic_auth/client.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/basic_auth/client.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/basic_auth/client.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb similarity index 91% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb index aa7884f2eb40..0229aeba3a22 100644 --- a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/client.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb @@ -9,7 +9,7 @@ class Client # @return [void] def initialize(username:, password:, base_url: nil) headers = { - "User-Agent" => "fern_basic-auth-optional/0.0.1", + "User-Agent" => "fern_basic-auth-pw-omitted/0.0.1", "X-Fern-Language" => "Ruby" } headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/api_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/api_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/api_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/client_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/client_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/client_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/redirect_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/redirect_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/redirect_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/response_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/response_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/response_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/server_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/server_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/server_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/timeout_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/timeout_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/timeout_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/types/unauthorized_request_error_body.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/errors/types/unauthorized_request_error_body.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/errors/types/unauthorized_request_error_body.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/errors/constraint_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/constraint_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/errors/constraint_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/errors/type_error.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/errors/type_error.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/errors/type_error.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/http/base_request.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/base_request.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/http/base_request.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/http/raw_client.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/http/raw_client.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/http/raw_client.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/cursor_item_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_item_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/cursor_item_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/cursor_page_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/cursor_page_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/cursor_page_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/item_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/item_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/item_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/offset_item_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_item_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/offset_item_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/offset_page_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/iterators/offset_page_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/iterators/offset_page_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/json/request.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/request.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/json/request.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/json/serializable.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/json/serializable.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/json/serializable.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_encoder.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_encoder.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_encoder.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_form_data.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_form_data.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_form_data_part.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_form_data_part.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_form_data_part.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_request.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/multipart/multipart_request.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/multipart/multipart_request.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/array.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/array.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/array.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/boolean.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/boolean.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/boolean.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/enum.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/enum.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/enum.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/hash.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/hash.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/hash.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/model.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/model.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/model/field.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/model/field.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/model/field.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/type.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/type.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/type.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/union.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/union.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/union.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/unknown.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/unknown.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/unknown.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/utils.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/internal/types/utils.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/internal/types/utils.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/version.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/lib/seed/version.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/version.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/reference.md b/seed/ruby-sdk-v2/basic-auth-pw-omitted/reference.md similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/reference.md rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/reference.md diff --git a/seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec b/seed/ruby-sdk-v2/basic-auth-pw-omitted/seed.gemspec similarity index 97% rename from seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/seed.gemspec index aff5ff0c3c1c..2897e62684cc 100644 --- a/seed/ruby-sdk-v2/basic-auth-optional/seed.gemspec +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/seed.gemspec @@ -6,7 +6,7 @@ require_relative "custom.gemspec" # NOTE: A handful of these fields are required as part of the Ruby specification. # You can change them here or overwrite them in the custom gemspec file. Gem::Specification.new do |spec| - spec.name = "fern_basic-auth-optional" + spec.name = "fern_basic-auth-pw-omitted" spec.authors = ["Seed"] spec.version = Seed::VERSION spec.summary = "Ruby client library for the Seed API" diff --git a/seed/ruby-sdk-v2/basic-auth-optional/snippet.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/snippet.json similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/snippet.json rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/snippet.json diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/custom.test.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/custom.test.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/custom.test.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/test_helper.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/test_helper.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/test_helper.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/iterators/test_cursor_item_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_cursor_item_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/iterators/test_cursor_item_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/iterators/test_offset_item_iterator.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/iterators/test_offset_item_iterator.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/iterators/test_offset_item_iterator.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_array.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_array.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_array.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_boolean.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_boolean.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_boolean.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_enum.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_enum.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_enum.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_hash.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_hash.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_hash.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_model.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_model.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_model.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_union.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_union.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_union.rb diff --git a/seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_utils.rb similarity index 100% rename from seed/ruby-sdk-v2/basic-auth-optional/test/unit/internal/types/test_utils.rb rename to seed/ruby-sdk-v2/basic-auth-pw-omitted/test/unit/internal/types/test_utils.rb 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 662530d10e9462656dc94b235863c1202a9b0b1c Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:04:01 +0000 Subject: [PATCH 09/20] fix(ruby-sdk): bump version to 1.2.0 (feat requires minor bump) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/ruby-v2/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/ruby-v2/sdk/versions.yml b/generators/ruby-v2/sdk/versions.yml index 7c1c3ff78f2a..97bfb423b537 100644 --- a/generators/ruby-v2/sdk/versions.yml +++ b/generators/ruby-v2/sdk/versions.yml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json -- version: 1.1.14 +- version: 1.2.0 changelogEntry: - summary: | Support omitting username or password from basic auth when configured via From 38173e7dd3d6d15d435c7f8bb904af11ec6ad2b5 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:24:21 +0000 Subject: [PATCH 10/20] fix(ruby-sdk): remove unnecessary type casts for usernameOmit/passwordOmit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/root-client/RootClientGenerator.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 161e0815869f..0db5bfed0044 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -124,10 +124,8 @@ export class RootClientGenerator extends FileGenerator; - const usernameOmitted = scheme.usernameOmit === true; - const passwordOmitted = scheme.passwordOmit === true; + const usernameOmitted = basicAuthScheme.usernameOmit === true; + const passwordOmitted = basicAuthScheme.passwordOmit === true; // Omitted fields use empty string directly const usernameExpr = usernameOmitted ? `""` : usernameName; const passwordExpr = passwordOmitted ? `""` : passwordName; @@ -366,9 +364,8 @@ export class RootClientGenerator extends FileGenerator; - const usernameOmitted = schemeRecord.usernameOmit === true; - const passwordOmitted = schemeRecord.passwordOmit === true; + const usernameOmitted = scheme.usernameOmit === true; + const passwordOmitted = scheme.passwordOmit === true; if (!usernameOmitted) { const usernameParam = ruby.parameters.keyword({ name: scheme.username.snakeCase.safeName, From 139a33ad30f753b20c4054df8cbca55fad26f280 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:27:28 +0000 Subject: [PATCH 11/20] revert: restore type casts for usernameOmit/passwordOmit (needed for @fern-fern/ir-sdk) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/root-client/RootClientGenerator.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 0db5bfed0044..161e0815869f 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -124,8 +124,10 @@ export class RootClientGenerator extends FileGenerator; + const usernameOmitted = scheme.usernameOmit === true; + const passwordOmitted = scheme.passwordOmit === true; // Omitted fields use empty string directly const usernameExpr = usernameOmitted ? `""` : usernameName; const passwordExpr = passwordOmitted ? `""` : passwordName; @@ -364,8 +366,9 @@ export class RootClientGenerator extends FileGenerator; + const usernameOmitted = schemeRecord.usernameOmit === true; + const passwordOmitted = schemeRecord.passwordOmit === true; if (!usernameOmitted) { const usernameParam = ruby.parameters.keyword({ name: scheme.username.snakeCase.safeName, From c484fcb94ccdfbf4493f2ed0e8eb4f339343e0f4 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:53:35 +0000 Subject: [PATCH 12/20] fix(ruby-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 | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 9da7f1332655..bee3fb2874d4 100644 --- a/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -213,16 +213,28 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; }): ruby.KeywordArgument[] { - return [ - ruby.keywordArgument({ - name: auth.username.snakeCase.safeName, - value: ruby.TypeLiteral.string(values.username) - }), - ruby.keywordArgument({ - name: auth.password.snakeCase.safeName, - value: ruby.TypeLiteral.string(values.password) - }) - ]; + // 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 args: ruby.KeywordArgument[] = []; + if (!usernameOmitted) { + args.push( + ruby.keywordArgument({ + name: auth.username.snakeCase.safeName, + value: ruby.TypeLiteral.string(values.username) + }) + ); + } + if (!passwordOmitted) { + args.push( + ruby.keywordArgument({ + name: auth.password.snakeCase.safeName, + value: ruby.TypeLiteral.string(values.password) + }) + ); + } + return args; } private getRootClientBearerAuthArgs({ From ba92a727a9778dd521db355f83dee5e1ea5e3cf4 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:19:11 +0000 Subject: [PATCH 13/20] refactor(ruby-sdk): simplify omit checks from === true to !! Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../dynamic-snippets/src/EndpointSnippetGenerator.ts | 4 ++-- .../ruby-v2/sdk/src/root-client/RootClientGenerator.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index bee3fb2874d4..c588817e6f33 100644 --- a/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -215,8 +215,8 @@ export class EndpointSnippetGenerator { }): ruby.KeywordArgument[] { // 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 args: ruby.KeywordArgument[] = []; if (!usernameOmitted) { args.push( diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 161e0815869f..c9930d70658c 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -126,8 +126,8 @@ export class RootClientGenerator extends FileGenerator; - const usernameOmitted = scheme.usernameOmit === true; - const passwordOmitted = scheme.passwordOmit === true; + const usernameOmitted = !!scheme.usernameOmit; + const passwordOmitted = !!scheme.passwordOmit; // Omitted fields use empty string directly const usernameExpr = usernameOmitted ? `""` : usernameName; const passwordExpr = passwordOmitted ? `""` : passwordName; @@ -367,8 +367,8 @@ export class RootClientGenerator extends FileGenerator; - const usernameOmitted = schemeRecord.usernameOmit === true; - const passwordOmitted = schemeRecord.passwordOmit === true; + const usernameOmitted = !!schemeRecord.usernameOmit; + const passwordOmitted = !!schemeRecord.passwordOmit; if (!usernameOmitted) { const usernameParam = ruby.parameters.keyword({ name: scheme.username.snakeCase.safeName, From 844fb071bcf95a0686b1e93007e03b43f5fd0464 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 14/20] 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 306b9460c310cc1319b3b5c23f81da0727d29046 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:20:40 +0000 Subject: [PATCH 15/20] fix(ruby-sdk): remove omitted password field from basic-auth-pw-omitted seed output Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md | 3 +-- .../dynamic-snippets/example0/snippet.rb | 1 - .../dynamic-snippets/example1/snippet.rb | 1 - .../dynamic-snippets/example2/snippet.rb | 1 - .../dynamic-snippets/example3/snippet.rb | 1 - .../dynamic-snippets/example4/snippet.rb | 1 - .../dynamic-snippets/example5/snippet.rb | 1 - .../dynamic-snippets/example6/snippet.rb | 1 - seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb | 5 ++--- 9 files changed, 3 insertions(+), 12 deletions(-) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md b/seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md index 39dcc2ae12d7..6f843fc1d63a 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/README.md @@ -29,8 +29,7 @@ Instantiate and use the client with the following: require "seed" client = Seed::Client.new( - username: "", - password: "" + username: "" ) client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb index 2d4035b00456..8b9ab0c730ed 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example0/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb index 2d4035b00456..8b9ab0c730ed 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example1/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb index 2d4035b00456..8b9ab0c730ed 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example2/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb index 37fb27d10d1a..e99bb741c146 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example3/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb index 37fb27d10d1a..e99bb741c146 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example4/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb index 37fb27d10d1a..e99bb741c146 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example5/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb index 37fb27d10d1a..e99bb741c146 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/dynamic-snippets/example6/snippet.rb @@ -2,7 +2,6 @@ client = Seed::Client.new( username: "", - password: "", base_url: "https://api.fern.com" ) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb index 0229aeba3a22..3c950b7f39cc 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/lib/seed/client.rb @@ -4,15 +4,14 @@ module Seed class Client # @param base_url [String, nil] # @param username [String] - # @param password [String] # # @return [void] - def initialize(username:, password:, base_url: nil) + def initialize(username:, base_url: nil) headers = { "User-Agent" => "fern_basic-auth-pw-omitted/0.0.1", "X-Fern-Language" => "Ruby" } - headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{password}")}" + headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{""}")}" @raw_client = Seed::Internal::Http::RawClient.new( base_url: base_url, headers: headers From 2b5bd20801e830e0c4528ff21c9b65e54bf802e5 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:16:13 +0000 Subject: [PATCH 16/20] fix(ruby-sdk): remove cosmetic #{""} from generated auth header When a field is omitted, the credential string is now built cleanly (e.g., "#{username}:" instead of "#{username}:#{""}"). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/root-client/RootClientGenerator.ts | 17 ++++++++++++----- .../basic-auth-pw-omitted/lib/seed/client.rb | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index 1f9c6caee200..688ff3160855 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -127,9 +127,16 @@ export class RootClientGenerator extends FileGenerator "fern_basic-auth-pw-omitted/0.0.1", "X-Fern-Language" => "Ruby" } - headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:#{""}")}" + headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:")}" @raw_client = Seed::Internal::Http::RawClient.new( base_url: base_url, headers: headers From d36ba63e5486a90cb2367276e97298c5e6aeef85 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:58:35 +0000 Subject: [PATCH 17/20] feat(ruby-sdk): add wire tests for basic-auth-pw-omitted fixture - Enable enableWireTests for basic-auth-pw-omitted in seed.yml - Fix WireTestGenerator.buildAuthParamsForSetup to skip omitted fields - Generated wire test correctly instantiates client with username only (no password param) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/wire-tests/WireTestGenerator.ts | 8 +- .../wire-tests/.fern/metadata.json | 10 + .../wire-tests/.github/workflows/ci.yml | 75 ++++++ .../wire-tests/.gitignore | 1 + .../wire-tests/.rubocop.yml | 69 ++++++ .../basic-auth-pw-omitted/wire-tests/Gemfile | 19 ++ .../wire-tests/Gemfile.custom | 14 ++ .../wire-tests/README.md | 157 +++++++++++++ .../basic-auth-pw-omitted/wire-tests/Rakefile | 20 ++ .../wire-tests/custom.gemspec.rb | 16 ++ .../dynamic-snippets/example0/snippet.rb | 8 + .../dynamic-snippets/example1/snippet.rb | 8 + .../dynamic-snippets/example2/snippet.rb | 8 + .../dynamic-snippets/example3/snippet.rb | 8 + .../dynamic-snippets/example4/snippet.rb | 8 + .../dynamic-snippets/example5/snippet.rb | 8 + .../dynamic-snippets/example6/snippet.rb | 8 + .../wire-tests/lib/seed.rb | 41 ++++ .../wire-tests/lib/seed/basic_auth/client.rb | 77 +++++++ .../wire-tests/lib/seed/client.rb | 26 +++ .../wire-tests/lib/seed/errors/api_error.rb | 8 + .../lib/seed/errors/client_error.rb | 17 ++ .../lib/seed/errors/redirect_error.rb | 8 + .../lib/seed/errors/response_error.rb | 42 ++++ .../lib/seed/errors/server_error.rb | 11 + .../lib/seed/errors/timeout_error.rb | 8 + .../types/unauthorized_request_error_body.rb | 11 + .../seed/internal/errors/constraint_error.rb | 10 + .../lib/seed/internal/errors/type_error.rb | 10 + .../lib/seed/internal/http/base_request.rb | 51 +++++ .../lib/seed/internal/http/raw_client.rb | 214 ++++++++++++++++++ .../iterators/cursor_item_iterator.rb | 28 +++ .../iterators/cursor_page_iterator.rb | 51 +++++ .../seed/internal/iterators/item_iterator.rb | 59 +++++ .../iterators/offset_item_iterator.rb | 30 +++ .../iterators/offset_page_iterator.rb | 83 +++++++ .../lib/seed/internal/json/request.rb | 41 ++++ .../lib/seed/internal/json/serializable.rb | 25 ++ .../internal/multipart/multipart_encoder.rb | 141 ++++++++++++ .../internal/multipart/multipart_form_data.rb | 78 +++++++ .../multipart/multipart_form_data_part.rb | 51 +++++ .../internal/multipart/multipart_request.rb | 40 ++++ .../lib/seed/internal/types/array.rb | 47 ++++ .../lib/seed/internal/types/boolean.rb | 34 +++ .../lib/seed/internal/types/enum.rb | 56 +++++ .../lib/seed/internal/types/hash.rb | 36 +++ .../lib/seed/internal/types/model.rb | 208 +++++++++++++++++ .../lib/seed/internal/types/model/field.rb | 38 ++++ .../lib/seed/internal/types/type.rb | 35 +++ .../lib/seed/internal/types/union.rb | 161 +++++++++++++ .../lib/seed/internal/types/unknown.rb | 15 ++ .../lib/seed/internal/types/utils.rb | 116 ++++++++++ .../wire-tests/lib/seed/version.rb | 5 + .../wire-tests/reference.md | 118 ++++++++++ .../wire-tests/seed.gemspec | 36 +++ .../wire-tests/snippet.json | 0 .../wire-tests/test/custom.test.rb | 15 ++ .../wire-tests/test/test_helper.rb | 3 + .../iterators/test_cursor_item_iterator.rb | 189 ++++++++++++++++ .../iterators/test_offset_item_iterator.rb | 151 ++++++++++++ .../test/unit/internal/types/test_array.rb | 37 +++ .../test/unit/internal/types/test_boolean.rb | 35 +++ .../test/unit/internal/types/test_enum.rb | 42 ++++ .../test/unit/internal/types/test_hash.rb | 50 ++++ .../test/unit/internal/types/test_model.rb | 154 +++++++++++++ .../test/unit/internal/types/test_union.rb | 62 +++++ .../test/unit/internal/types/test_utils.rb | 212 +++++++++++++++++ .../wire-tests/test/wire/basic_auth_test.rb | 50 ++++ .../wire-tests/test/wire/wire_helper.rb | 31 +++ .../test/wire/wiremock_test_case.rb | 49 ++++ .../wiremock/docker-compose.test.yml | 14 ++ .../wiremock/wiremock-mappings.json | 60 +++++ seed/ruby-sdk-v2/seed.yml | 4 + 73 files changed, 3667 insertions(+), 2 deletions(-) create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.fern/metadata.json create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.github/workflows/ci.yml create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.gitignore create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.rubocop.yml create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile.custom create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/README.md create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Rakefile create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/custom.gemspec.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example0/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example1/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example2/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example3/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example4/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example5/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example6/snippet.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/basic_auth/client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/api_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/client_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/redirect_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/response_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/server_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/timeout_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/types/unauthorized_request_error_body.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/constraint_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/type_error.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/base_request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/raw_client.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_page_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_page_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/serializable.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_encoder.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data_part.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_request.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/array.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/boolean.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/enum.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/hash.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model/field.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/type.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/union.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/unknown.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/utils.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/version.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/reference.md create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/seed.gemspec create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/snippet.json create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/custom.test.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/test_helper.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_cursor_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_offset_item_iterator.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_array.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_boolean.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_enum.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_hash.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_model.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_union.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_utils.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wire_helper.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/docker-compose.test.yml create mode 100644 seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json diff --git a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts index fdf1cdea876e..47bc8c46758f 100644 --- a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts +++ b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts @@ -210,8 +210,12 @@ export class WireTestGenerator { authParams.push(`${this.case.snakeSafe(scheme.name)}: "test-api-key"`); break; case "basic": - authParams.push(`${this.case.snakeSafe(scheme.username)}: "test-username"`); - authParams.push(`${this.case.snakeSafe(scheme.password)}: "test-password"`); + if (!scheme.usernameOmit) { + authParams.push(`${this.case.snakeSafe(scheme.username)}: "test-username"`); + } + if (!scheme.passwordOmit) { + authParams.push(`${this.case.snakeSafe(scheme.password)}: "test-password"`); + } break; case "oauth": authParams.push('client_id: "test-client-id"'); diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.fern/metadata.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.fern/metadata.json new file mode 100644 index 000000000000..c22a966f974c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.fern/metadata.json @@ -0,0 +1,10 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-ruby-sdk-v2", + "generatorVersion": "latest", + "generatorConfig": { + "enableWireTests": true + }, + "originGitCommit": "DUMMY", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.github/workflows/ci.yml b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.github/workflows/ci.yml new file mode 100644 index 000000000000..9a224d6dfd0d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: ci + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Run Rubocop + run: bundle exec rubocop + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Run Tests + run: RUN_WIRE_TESTS=true bundle exec rake test + + publish: + name: Publish to RubyGems.org + runs-on: ubuntu-latest + needs: [lint, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + + permissions: + id-token: write + contents: write + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - name: Install dependencies + run: bundle install + + - name: Configure RubyGems credentials + uses: rubygems/configure-rubygems-credentials@v1.0.0 + + - name: Build gem + run: bundle exec rake build + + - name: Push gem to RubyGems + run: gem push pkg/*.gem diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.gitignore b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.gitignore new file mode 100644 index 000000000000..c111b331371a --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.gitignore @@ -0,0 +1 @@ +*.gem diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.rubocop.yml b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.rubocop.yml new file mode 100644 index 000000000000..75d8f836f2f0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/.rubocop.yml @@ -0,0 +1,69 @@ +plugins: + - rubocop-minitest + +AllCops: + TargetRubyVersion: 3.3 + NewCops: enable + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/AccessModifierDeclarations: + Enabled: false + +Lint/ConstantDefinitionInBlock: + Enabled: false + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ParameterLists: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Layout/LineLength: + Enabled: false + +Naming/VariableNumber: + EnforcedStyle: normalcase + +Style/Documentation: + Enabled: false + +Style/Lambda: + EnforcedStyle: literal + +Minitest/MultipleAssertions: + Enabled: false + +Minitest/UselessAssertion: + Enabled: false + +# Dynamic snippets are code samples for documentation, not standalone Ruby files. +Style/FrozenStringLiteralComment: + Exclude: + - "dynamic-snippets/**/*" + +Layout/FirstHashElementIndentation: + Exclude: + - "dynamic-snippets/**/*" diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile new file mode 100644 index 000000000000..16877f89f300 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +group :test, :development do + gem "minitest", "~> 5.16" + gem "minitest-rg" + gem "pry" + gem "rake", "~> 13.0" + gem "rubocop", "~> 1.21" + gem "rubocop-minitest" + gem "webmock" +end + +# Load custom Gemfile configuration if it exists +custom_gemfile = File.join(__dir__, "Gemfile.custom") +eval_gemfile(custom_gemfile) if File.exist?(custom_gemfile) diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile.custom b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile.custom new file mode 100644 index 000000000000..11bdfaf13f2d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Gemfile.custom @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Custom Gemfile configuration file +# This file is automatically loaded by the main Gemfile. You can add custom gems, +# groups, or other Gemfile configurations here. If you do make changes to this file, +# you will need to add it to the .fernignore file to prevent your changes from being +# overwritten by the generator. + +# Example usage: +# group :test, :development do +# gem 'custom-gem', '~> 2.0' +# end + +# Add your custom gem dependencies here \ No newline at end of file diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/README.md b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/README.md new file mode 100644 index 000000000000..39dcc2ae12d7 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/README.md @@ -0,0 +1,157 @@ +# Seed Ruby 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%2FRuby) + +The Seed Ruby library provides convenient access to the Seed APIs from Ruby. + +## Table of Contents + +- [Reference](#reference) +- [Usage](#usage) +- [Environments](#environments) +- [Errors](#errors) +- [Advanced](#advanced) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) +- [Contributing](#contributing) + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```ruby +require "seed" + +client = Seed::Client.new( + username: "", + password: "" +) + +client.basic_auth.post_with_basic_auth +``` + +## Environments + +This SDK allows you to configure different custom URLs for API requests. You can specify your own custom URL. + +### Custom URL +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com" +) +``` + +## Errors + +Failed API calls will raise errors that can be rescued from granularly. + +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com" +) + +begin + result = client.basic_auth.post_with_basic_auth +rescue Seed::Errors::TimeoutError + puts "API didn't respond before our timeout elapsed" +rescue Seed::Errors::ServiceUnavailableError + puts "API returned status 503, is probably overloaded, try again later" +rescue Seed::Errors::ServerError + puts "API returned some other 5xx status, this is probably a bug" +rescue Seed::Errors::ResponseError => e + puts "API returned an unexpected status other than 5xx: #{e.code} #{e.message}" +rescue Seed::Errors::ApiError => e + puts "Some other error occurred when calling the API: #{e.message}" +end +``` + +## Advanced + +### Retries + +The SDK is instrumented with automatic retries. 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) + +Use the `max_retries` option to configure this behavior. + +```ruby +require "seed" + +client = Seed::Client.new( + base_url: "https://example.com", + max_retries: 3 # Configure max retries (default is 2) +) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. Use the `timeout` option to configure this behavior. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + timeout: 30 # 30 second timeout +) +``` + +### Additional Headers + +If you would like to send additional headers as part of the request, use the `additional_headers` request option. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + request_options: { + additional_headers: { + "X-Custom-Header" => "custom-value" + } + } +) +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `additional_query_parameters` request option. + +```ruby +require "seed" + +response = client.basic_auth.post_with_basic_auth( + ..., + request_options: { + additional_query_parameters: { + "custom_param" => "custom-value" + } + } +) +``` + +## 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/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Rakefile b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Rakefile new file mode 100644 index 000000000000..9bdd4a6ce80b --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/Rakefile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "minitest/test_task" + +Minitest::TestTask.create + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[test] + +task lint: %i[rubocop] + +# Run only the custom test file +Minitest::TestTask.create(:customtest) do |t| + t.libs << "test" + t.test_globs = ["test/custom.test.rb"] +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/custom.gemspec.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/custom.gemspec.rb new file mode 100644 index 000000000000..86d8efd3cd3c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/custom.gemspec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Custom gemspec configuration file +# This file is automatically loaded by the main gemspec file. The 'spec' variable is available +# in this context from the main gemspec file. You can modify this file to add custom metadata, +# dependencies, or other gemspec configurations. If you do make changes to this file, you will +# need to add it to the .fernignore file to prevent your changes from being overwritten. + +def add_custom_gemspec_data(spec) + # Example custom configurations (uncomment and modify as needed) + + # spec.authors = ["Your name"] + # spec.email = ["your.email@example.com"] + # spec.homepage = "https://github.com/your-org/seed-ruby" + # spec.license = "Your license" +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example0/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example0/snippet.rb new file mode 100644 index 000000000000..8b9ab0c730ed --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example0/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example1/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example1/snippet.rb new file mode 100644 index 000000000000..8b9ab0c730ed --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example1/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example2/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example2/snippet.rb new file mode 100644 index 000000000000..8b9ab0c730ed --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example2/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.get_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example3/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example3/snippet.rb new file mode 100644 index 000000000000..e99bb741c146 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example3/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example4/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example4/snippet.rb new file mode 100644 index 000000000000..e99bb741c146 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example4/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example5/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example5/snippet.rb new file mode 100644 index 000000000000..e99bb741c146 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example5/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example6/snippet.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example6/snippet.rb new file mode 100644 index 000000000000..e99bb741c146 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/dynamic-snippets/example6/snippet.rb @@ -0,0 +1,8 @@ +require "seed" + +client = Seed::Client.new( + username: "", + base_url: "https://api.fern.com" +) + +client.basic_auth.post_with_basic_auth diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed.rb new file mode 100644 index 000000000000..3b8182547ec3 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "json" +require "net/http" +require "securerandom" +require "base64" + +require_relative "seed/internal/json/serializable" +require_relative "seed/internal/types/type" +require_relative "seed/internal/types/utils" +require_relative "seed/internal/types/union" +require_relative "seed/internal/errors/constraint_error" +require_relative "seed/internal/errors/type_error" +require_relative "seed/internal/http/base_request" +require_relative "seed/internal/json/request" +require_relative "seed/internal/http/raw_client" +require_relative "seed/internal/multipart/multipart_encoder" +require_relative "seed/internal/multipart/multipart_form_data_part" +require_relative "seed/internal/multipart/multipart_form_data" +require_relative "seed/internal/multipart/multipart_request" +require_relative "seed/internal/types/model/field" +require_relative "seed/internal/types/model" +require_relative "seed/internal/types/array" +require_relative "seed/internal/types/boolean" +require_relative "seed/internal/types/enum" +require_relative "seed/internal/types/hash" +require_relative "seed/internal/types/unknown" +require_relative "seed/errors/api_error" +require_relative "seed/errors/response_error" +require_relative "seed/errors/client_error" +require_relative "seed/errors/redirect_error" +require_relative "seed/errors/server_error" +require_relative "seed/errors/timeout_error" +require_relative "seed/internal/iterators/item_iterator" +require_relative "seed/internal/iterators/cursor_item_iterator" +require_relative "seed/internal/iterators/offset_item_iterator" +require_relative "seed/internal/iterators/cursor_page_iterator" +require_relative "seed/internal/iterators/offset_page_iterator" +require_relative "seed/errors/types/unauthorized_request_error_body" +require_relative "seed/client" +require_relative "seed/basic_auth/client" diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/basic_auth/client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/basic_auth/client.rb new file mode 100644 index 000000000000..e90716fb53a6 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/basic_auth/client.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Seed + module BasicAuth + class Client + # @param client [Seed::Internal::Http::RawClient] + # + # @return [void] + def initialize(client:) + @client = client + end + + # GET request with basic auth scheme + # + # @param request_options [Hash] + # @param params [Hash] + # @option request_options [String] :base_url + # @option request_options [Hash{String => Object}] :additional_headers + # @option request_options [Hash{String => Object}] :additional_query_parameters + # @option request_options [Hash{String => Object}] :additional_body_parameters + # @option request_options [Integer] :timeout_in_seconds + # + # @return [Boolean] + def get_with_basic_auth(request_options: {}, **params) + Seed::Internal::Types::Utils.normalize_keys(params) + request = Seed::Internal::JSON::Request.new( + base_url: request_options[:base_url], + method: "GET", + path: "basic-auth", + request_options: request_options + ) + begin + response = @client.send(request) + rescue Net::HTTPRequestTimeout + raise Seed::Errors::TimeoutError + end + code = response.code.to_i + return if code.between?(200, 299) + + error_class = Seed::Errors::ResponseError.subclass_for_code(code) + raise error_class.new(response.body, code: code) + end + + # POST request with basic auth scheme + # + # @param request_options [Hash] + # @param params [Hash] + # @option request_options [String] :base_url + # @option request_options [Hash{String => Object}] :additional_headers + # @option request_options [Hash{String => Object}] :additional_query_parameters + # @option request_options [Hash{String => Object}] :additional_body_parameters + # @option request_options [Integer] :timeout_in_seconds + # + # @return [Boolean] + def post_with_basic_auth(request_options: {}, **params) + params = Seed::Internal::Types::Utils.normalize_keys(params) + request = Seed::Internal::JSON::Request.new( + base_url: request_options[:base_url], + method: "POST", + path: "basic-auth", + body: params, + request_options: request_options + ) + begin + response = @client.send(request) + rescue Net::HTTPRequestTimeout + raise Seed::Errors::TimeoutError + end + code = response.code.to_i + return if code.between?(200, 299) + + error_class = Seed::Errors::ResponseError.subclass_for_code(code) + raise error_class.new(response.body, code: code) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/client.rb new file mode 100644 index 000000000000..aba6234f5885 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/client.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Seed + class Client + # @param base_url [String, nil] + # @param username [String] + # + # @return [void] + def initialize(username:, base_url: nil) + headers = { + "User-Agent" => "fern_basic-auth-pw-omitted/0.0.1", + "X-Fern-Language" => "Ruby" + } + headers["Authorization"] = "Basic #{Base64.strict_encode64("#{username}:")}" + @raw_client = Seed::Internal::Http::RawClient.new( + base_url: base_url, + headers: headers + ) + end + + # @return [Seed::BasicAuth::Client] + def basic_auth + @basic_auth ||= Seed::BasicAuth::Client.new(client: @raw_client) + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/api_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/api_error.rb new file mode 100644 index 000000000000..b8ba53889b36 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/api_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ApiError < StandardError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/client_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/client_error.rb new file mode 100644 index 000000000000..c3c6033641e2 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/client_error.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ClientError < ResponseError + end + + class UnauthorizedError < ClientError + end + + class ForbiddenError < ClientError + end + + class NotFoundError < ClientError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/redirect_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/redirect_error.rb new file mode 100644 index 000000000000..f663c01e7615 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/redirect_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class RedirectError < ResponseError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/response_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/response_error.rb new file mode 100644 index 000000000000..beb4a1baf959 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/response_error.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ResponseError < ApiError + attr_reader :code + + def initialize(msg, code:) + @code = code + super(msg) + end + + def inspect + "#<#{self.class.name} @code=#{code} @body=#{message}>" + end + + # Returns the most appropriate error class for the given code. + # + # @return [Class] + def self.subclass_for_code(code) + case code + when 300..399 + RedirectError + when 401 + UnauthorizedError + when 403 + ForbiddenError + when 404 + NotFoundError + when 400..499 + ClientError + when 503 + ServiceUnavailableError + when 500..599 + ServerError + else + ResponseError + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/server_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/server_error.rb new file mode 100644 index 000000000000..1838027cdeab --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/server_error.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Seed + module Errors + class ServerError < ResponseError + end + + class ServiceUnavailableError < ApiError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/timeout_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/timeout_error.rb new file mode 100644 index 000000000000..ec3a24bb7e96 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/timeout_error.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Seed + module Errors + class TimeoutError < ApiError + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/types/unauthorized_request_error_body.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/types/unauthorized_request_error_body.rb new file mode 100644 index 000000000000..a3caea8d9bea --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/errors/types/unauthorized_request_error_body.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Seed + module Errors + module Types + class UnauthorizedRequestErrorBody < Internal::Types::Model + field :message, -> { String }, optional: false, nullable: false + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/constraint_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/constraint_error.rb new file mode 100644 index 000000000000..e2f0bd66ac37 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/constraint_error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Errors + class ConstraintError < StandardError + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/type_error.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/type_error.rb new file mode 100644 index 000000000000..6aec80f59f05 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/errors/type_error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Errors + class TypeError < StandardError + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/base_request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/base_request.rb new file mode 100644 index 000000000000..d35df463e5b0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/base_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Http + # @api private + class BaseRequest + attr_reader :base_url, :path, :method, :headers, :query, :request_options + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [String] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, request_options: {}) + @base_url = base_url + @path = path + @method = method + @headers = headers + @query = query + @request_options = request_options + end + + # @return [Hash] The query parameters merged with additional query parameters from request options. + def encode_query + additional_query = @request_options&.dig(:additional_query_parameters) || @request_options&.dig("additional_query_parameters") || {} + @query.merge(additional_query) + end + + # Child classes should implement: + # - encode_headers: Returns the encoded HTTP request headers. + # - encode_body: Returns the encoded HTTP request body. + + private + + # Merges additional_headers from request_options into sdk_headers, filtering out + # any keys that collide with SDK-set or client-protected headers (case-insensitive). + # @param sdk_headers [Hash] Headers set by the SDK for this request type. + # @param protected_keys [Array] Additional header keys that must not be overridden. + # @return [Hash] The merged headers. + def merge_additional_headers(sdk_headers, protected_keys: []) + additional_headers = @request_options&.dig(:additional_headers) || @request_options&.dig("additional_headers") || {} + all_protected = (sdk_headers.keys + protected_keys).to_set { |k| k.to_s.downcase } + filtered = additional_headers.reject { |key, _| all_protected.include?(key.to_s.downcase) } + sdk_headers.merge(filtered) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/raw_client.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/raw_client.rb new file mode 100644 index 000000000000..482ab9517714 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/http/raw_client.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Http + # @api private + class RawClient + # Default HTTP status codes that trigger a retry + RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504, 521, 522, 524].freeze + # Initial delay between retries in seconds + INITIAL_RETRY_DELAY = 0.5 + # Maximum delay between retries in seconds + MAX_RETRY_DELAY = 60.0 + # Jitter factor for randomizing retry delays (20%) + JITTER_FACTOR = 0.2 + + # @return [String] The base URL for requests + attr_reader :base_url + + # @param base_url [String] The base url for the request. + # @param max_retries [Integer] The number of times to retry a failed request, defaults to 2. + # @param timeout [Float] The timeout for the request, defaults to 60.0 seconds. + # @param headers [Hash] The headers for the request. + def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}) + @base_url = base_url + @max_retries = max_retries + @timeout = timeout + @default_headers = { + "X-Fern-Language": "Ruby", + "X-Fern-SDK-Name": "seed", + "X-Fern-SDK-Version": "0.0.1" + }.merge(headers) + end + + # @param request [Seed::Internal::Http::BaseRequest] The HTTP request. + # @return [HTTP::Response] The HTTP response. + def send(request) + url = build_url(request) + attempt = 0 + response = nil + + loop do + http_request = build_http_request( + url:, + method: request.method, + headers: request.encode_headers(protected_keys: @default_headers.keys), + body: request.encode_body + ) + + conn = connect(url) + conn.open_timeout = @timeout + conn.read_timeout = @timeout + conn.write_timeout = @timeout + conn.continue_timeout = @timeout + + response = conn.request(http_request) + + break unless should_retry?(response, attempt) + + delay = retry_delay(response, attempt) + sleep(delay) + attempt += 1 + end + + response + end + + # Determines if a request should be retried based on the response status code. + # @param response [Net::HTTPResponse] The HTTP response. + # @param attempt [Integer] The current retry attempt (0-indexed). + # @return [Boolean] Whether the request should be retried. + def should_retry?(response, attempt) + return false if attempt >= @max_retries + + status = response.code.to_i + RETRYABLE_STATUSES.include?(status) + end + + # Calculates the delay before the next retry attempt using exponential backoff with jitter. + # Respects Retry-After header if present. + # @param response [Net::HTTPResponse] The HTTP response. + # @param attempt [Integer] The current retry attempt (0-indexed). + # @return [Float] The delay in seconds before the next retry. + def retry_delay(response, attempt) + # Check for Retry-After header (can be seconds or HTTP date) + retry_after = response["Retry-After"] + if retry_after + delay = parse_retry_after(retry_after) + return [delay, MAX_RETRY_DELAY].min if delay&.positive? + end + + # Exponential backoff with jitter: base_delay * 2^attempt + base_delay = INITIAL_RETRY_DELAY * (2**attempt) + add_jitter([base_delay, MAX_RETRY_DELAY].min) + end + + # Parses the Retry-After header value. + # @param value [String] The Retry-After header value (seconds or HTTP date). + # @return [Float, nil] The delay in seconds, or nil if parsing fails. + def parse_retry_after(value) + # Try parsing as integer (seconds) + seconds = Integer(value, exception: false) + return seconds.to_f if seconds + + # Try parsing as HTTP date + begin + retry_time = Time.httpdate(value) + delay = retry_time - Time.now + delay.positive? ? delay : nil + rescue ArgumentError + nil + end + end + + # Adds random jitter to a delay value. + # @param delay [Float] The base delay in seconds. + # @return [Float] The delay with jitter applied. + def add_jitter(delay) + jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2 + [delay + jitter, 0].max + end + + LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze + + # @param request [Seed::Internal::Http::BaseRequest] The HTTP request. + # @return [URI::Generic] The URL. + def build_url(request) + encoded_query = request.encode_query + + # If the path is already an absolute URL, use it directly + if request.path.start_with?("http://", "https://") + url = request.path + url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any? + parsed = URI.parse(url) + validate_https!(parsed) + return parsed + end + + path = request.path.start_with?("/") ? request.path[1..] : request.path + base = request.base_url || @base_url + url = "#{base.chomp("/")}/#{path}" + url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any? + parsed = URI.parse(url) + validate_https!(parsed) + parsed + end + + # Raises if the URL uses http:// for a non-localhost host, which would + # send authentication credentials in plaintext. + # @param url [URI::Generic] The parsed URL. + def validate_https!(url) + return if url.scheme != "http" + return if LOCALHOST_HOSTS.include?(url.host) + + raise ArgumentError, "Refusing to send request to non-HTTPS URL: #{url}. " \ + "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL." + end + + # @param url [URI::Generic] The url to the resource. + # @param method [String] The HTTP method to use. + # @param headers [Hash] The headers for the request. + # @param body [String, nil] The body for the request. + # @return [HTTP::Request] The HTTP request. + def build_http_request(url:, method:, headers: {}, body: nil) + request = Net::HTTPGenericRequest.new( + method, + !body.nil?, + method != "HEAD", + url + ) + + request_headers = @default_headers.merge(headers) + request_headers.each { |name, value| request[name] = value } + request.body = body if body + + request + end + + # @param query [Hash] The query for the request. + # @return [String, nil] The encoded query. + def encode_query(query) + query.to_h.empty? ? nil : URI.encode_www_form(query) + end + + # @param url [URI::Generic] The url to connect to. + # @return [Net::HTTP] The HTTP connection. + def connect(url) + is_https = (url.scheme == "https") + + port = if url.port + url.port + elsif is_https + Net::HTTP.https_default_port + else + Net::HTTP.http_default_port + end + + http = Net::HTTP.new(url.host, port) + http.use_ssl = is_https + http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https + # NOTE: We handle retries at the application level with HTTP status code awareness, + # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries. + http.max_retries = 0 + http + end + + # @return [String] + def inspect + "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_item_iterator.rb new file mode 100644 index 000000000000..ab627ffc7025 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_item_iterator.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Seed + module Internal + class CursorItemIterator < ItemIterator + # Instantiates a CursorItemIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields individual items from it. + # + # @param initial_cursor [String] The initial cursor to use when iterating, if any. + # @param cursor_field [Symbol] The field in API responses to extract the next cursor from. + # @param item_field [Symbol] The field in API responses to extract the items to iterate over. + # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API. + # @return [Seed::Internal::CursorItemIterator] + def initialize(initial_cursor:, cursor_field:, item_field:, &) + super() + @item_field = item_field + @page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &) + @page = nil + end + + # Returns the CursorPageIterator mediating access to the underlying API. + # + # @return [Seed::Internal::CursorPageIterator] + def pages + @page_iterator + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_page_iterator.rb new file mode 100644 index 000000000000..f479a749fef9 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/cursor_page_iterator.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Seed + module Internal + class CursorPageIterator + include Enumerable + + # Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields pages of items. + # + # @param initial_cursor [String] The initial cursor to use when iterating, if any. + # @param cursor_field [Symbol] The name of the field in API responses to extract the next cursor from. + # @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API. + # @return [Seed::Internal::CursorPageIterator] + def initialize(initial_cursor:, cursor_field:, &block) + @need_initial_load = initial_cursor.nil? + @cursor = initial_cursor + @cursor_field = cursor_field + @get_next_page = block + end + + # Iterates over each page returned by the API. + # + # @param block [Proc] The block which each retrieved page is yielded to. + # @return [NilClass] + def each(&block) + while (page = next_page) + block.call(page) + end + end + + # Whether another page will be available from the API. + # + # @return [Boolean] + def next? + @need_initial_load || !@cursor.nil? + end + + # Retrieves the next page from the API. + # + # @return [Boolean] + def next_page + return if !@need_initial_load && @cursor.nil? + + @need_initial_load = false + fetched_page = @get_next_page.call(@cursor) + @cursor = fetched_page.send(@cursor_field) + fetched_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/item_iterator.rb new file mode 100644 index 000000000000..1284fb0fd367 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/item_iterator.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Seed + module Internal + class ItemIterator + include Enumerable + + # Iterates over each item returned by the API. + # + # @param block [Proc] The block which each retrieved item is yielded to. + # @return [NilClass] + def each(&block) + while (item = next_element) + block.call(item) + end + end + + # Whether another item will be available from the API. + # + # @return [Boolean] + def next? + load_next_page if @page.nil? + return false if @page.nil? + + return true if any_items_in_cached_page? + + load_next_page + any_items_in_cached_page? + end + + # Retrieves the next item from the API. + def next_element + item = next_item_from_cached_page + return item if item + + load_next_page + next_item_from_cached_page + end + + private + + def next_item_from_cached_page + return unless @page + + @page.send(@item_field).shift + end + + def any_items_in_cached_page? + return false unless @page + + !@page.send(@item_field).empty? + end + + def load_next_page + @page = @page_iterator.next_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_item_iterator.rb new file mode 100644 index 000000000000..f8840246686d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_item_iterator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Seed + module Internal + class OffsetItemIterator < ItemIterator + # Instantiates an OffsetItemIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields the individual items from it. + # + # @param initial_page [Integer] The initial page or offset to start from when iterating. + # @param item_field [Symbol] The name of the field in API responses to extract the items to iterate over. + # @param has_next_field [Symbol] The name of the field in API responses containing a boolean of whether another page exists. + # @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1) + # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API. + # + # @return [Seed::Internal::OffsetItemIterator] + def initialize(initial_page:, item_field:, has_next_field:, step:, &) + super() + @item_field = item_field + @page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &) + @page = nil + end + + # Returns the OffsetPageIterator that is mediating access to the underlying API. + # + # @return [Seed::Internal::OffsetPageIterator] + def pages + @page_iterator + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_page_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_page_iterator.rb new file mode 100644 index 000000000000..051b65c5774c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/iterators/offset_page_iterator.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Seed + module Internal + class OffsetPageIterator + include Enumerable + + # Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it. + # + # @param initial_page [Integer] The initial page to use when iterating, if any. + # @param item_field [Symbol] The field to pull the list of items to iterate over. + # @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any. + # @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1) + # @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API. + # @return [Seed::Internal::OffsetPageIterator] + def initialize(initial_page:, item_field:, has_next_field:, step:, &block) + @page_number = initial_page || (step ? 0 : 1) + @item_field = item_field + @has_next_field = has_next_field + @step = step + @get_next_page = block + + # A cache of whether the API has another page, if it gives us that information... + @next_page = nil + # ...or the actual next page, preloaded, if it doesn't. + @has_next_page = nil + end + + # Iterates over each page returned by the API. + # + # @param block [Proc] The block which each retrieved page is yielded to. + # @return [NilClass] + def each(&block) + while (page = next_page) + block.call(page) + end + end + + # Whether another page will be available from the API. + # + # @return [Boolean] + def next? + return @has_next_page unless @has_next_page.nil? + return true if @next_page + + fetched_page = @get_next_page.call(@page_number) + fetched_page_items = fetched_page&.send(@item_field) + if fetched_page_items.nil? || fetched_page_items.empty? + @has_next_page = false + else + @next_page = fetched_page + true + end + end + + # Returns the next page from the API. + def next_page + return nil if @page_number.nil? + + if @next_page + this_page = @next_page + @next_page = nil + else + this_page = @get_next_page.call(@page_number) + end + + @has_next_page = this_page&.send(@has_next_field) if @has_next_field + + items = this_page.send(@item_field) + if items.nil? || items.empty? + @page_number = nil + return nil + elsif @step + @page_number += items.length + else + @page_number += 1 + end + + this_page + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/request.rb new file mode 100644 index 000000000000..667ceae8ac59 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/request.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Seed + module Internal + module JSON + # @api private + class Request < Seed::Internal::Http::BaseRequest + attr_reader :body + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [Symbol] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param body [Object, nil] The JSON request body (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {}) + super(base_url:, path:, method:, headers:, query:, request_options:) + + @body = body + end + + # @return [Hash] The encoded HTTP request headers. + # @param protected_keys [Array] Header keys set by the SDK client (e.g. auth, metadata) + # that must not be overridden by additional_headers from request_options. + def encode_headers(protected_keys: []) + sdk_headers = { + "Content-Type" => "application/json", + "Accept" => "application/json" + }.merge(@headers) + merge_additional_headers(sdk_headers, protected_keys:) + end + + # @return [String, nil] The encoded HTTP request body. + def encode_body + @body.nil? ? nil : ::JSON.generate(@body) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/serializable.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/serializable.rb new file mode 100644 index 000000000000..f80a15fb962c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/json/serializable.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Seed + module Internal + module JSON + module Serializable + # Loads data from JSON into its deserialized form + # + # @param str [String] Raw JSON to load into an object + # @return [Object] + def load(str) + raise NotImplementedError + end + + # Dumps data from its deserialized form into JSON + # + # @param value [Object] The deserialized value + # @return [String] + def dump(value) + raise NotImplementedError + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_encoder.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_encoder.rb new file mode 100644 index 000000000000..307ad7436a57 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_encoder.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # Encodes parameters into a `multipart/form-data` payload as described by RFC + # 2388: + # + # https://tools.ietf.org/html/rfc2388 + # + # This is most useful for transferring file-like objects. + # + # Parameters should be added with `#encode`. When ready, use `#body` to get + # the encoded result and `#content_type` to get the value that should be + # placed in the `Content-Type` header of a subsequent request (which includes + # a boundary value). + # + # This abstraction is heavily inspired by Stripe's multipart/form-data implementation, + # which can be found here: + # + # https://github.com/stripe/stripe-ruby/blob/ca00b676f04ac421cf5cb5ff0325f243651677b6/lib/stripe/multipart_encoder.rb#L18 + # + # @api private + class Encoder + CONTENT_TYPE = "multipart/form-data" + CRLF = "\r\n" + + attr_reader :boundary, :body + + def initialize + # Chose the same number of random bytes that Go uses in its standard + # library implementation. Easily enough entropy to ensure that it won't + # be present in a file we're sending. + @boundary = SecureRandom.hex(30) + + @body = String.new + @closed = false + @first_field = true + end + + # Gets the content type string including the boundary. + # + # @return [String] The content type with boundary + def content_type + "#{CONTENT_TYPE}; boundary=#{@boundary}" + end + + # Encode the given FormData object into a multipart/form-data payload. + # + # @param form_data [FormData] The form data to encode + # @return [String] The encoded body. + def encode(form_data) + return "" if form_data.parts.empty? + + form_data.parts.each do |part| + write_part(part) + end + close + + @body + end + + # Writes a FormDataPart to the encoder. + # + # @param part [FormDataPart] The part to write + # @return [nil] + def write_part(part) + raise "Cannot write to closed encoder" if @closed + + write_field( + name: part.name, + data: part.contents, + filename: part.filename, + headers: part.headers + ) + + nil + end + + # Writes a field to the encoder. + # + # @param name [String] The field name + # @param data [String] The field data + # @param filename [String, nil] Optional filename + # @param headers [Hash, nil] Optional additional headers + # @return [nil] + def write_field(name:, data:, filename: nil, headers: nil) + raise "Cannot write to closed encoder" if @closed + + if @first_field + @first_field = false + else + @body << CRLF + end + + @body << "--#{@boundary}#{CRLF}" + @body << %(Content-Disposition: form-data; name="#{escape(name.to_s)}") + @body << %(; filename="#{escape(filename)}") if filename + @body << CRLF + + if headers + headers.each do |key, value| + @body << "#{key}: #{value}#{CRLF}" + end + elsif filename + # Default content type for files. + @body << "Content-Type: application/octet-stream#{CRLF}" + end + + @body << CRLF + @body << data.to_s + + nil + end + + # Finalizes the encoder by writing the final boundary. + # + # @return [nil] + def close + raise "Encoder already closed" if @closed + + @body << CRLF + @body << "--#{@boundary}--" + @closed = true + + nil + end + + private + + # Escapes quotes for use in header values and replaces line breaks with spaces. + # + # @param str [String] The string to escape + # @return [String] The escaped string + def escape(str) + str.to_s.gsub('"', "%22").tr("\n", " ").tr("\r", " ") + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data.rb new file mode 100644 index 000000000000..5be1bb25341f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # @api private + class FormData + # @return [Array] The parts in this multipart form data. + attr_reader :parts + + # @return [Encoder] The encoder for this multipart form data. + private attr_reader :encoder + + def initialize + @encoder = Encoder.new + @parts = [] + end + + # Adds a new part to the multipart form data. + # + # @param name [String] The name of the form field + # @param value [String, Integer, Float, Boolean, #read] The value of the field + # @param content_type [String, nil] Optional content type + # @return [self] Returns self for chaining + def add(name:, value:, content_type: nil) + headers = content_type ? { "Content-Type" => content_type } : nil + add_part(FormDataPart.new(name:, value:, headers:)) + end + + # Adds a file to the multipart form data. + # + # @param name [String] The name of the form field + # @param file [#read] The file or readable object + # @param filename [String, nil] Optional filename (defaults to basename of path for File objects) + # @param content_type [String, nil] Optional content type (e.g. "image/png") + # @return [self] Returns self for chaining + def add_file(name:, file:, filename: nil, content_type: nil) + headers = content_type ? { "Content-Type" => content_type } : nil + filename ||= filename_for(file) + add_part(FormDataPart.new(name:, value: file, filename:, headers:)) + end + + # Adds a pre-created part to the multipart form data. + # + # @param part [FormDataPart] The part to add + # @return [self] Returns self for chaining + def add_part(part) + @parts << part + self + end + + # Gets the content type string including the boundary. + # + # @return [String] The content type with boundary. + def content_type + @encoder.content_type + end + + # Encode the multipart form data into a multipart/form-data payload. + # + # @return [String] The encoded body. + def encode + @encoder.encode(self) + end + + private + + def filename_for(file) + if file.is_a?(::File) || file.respond_to?(:path) + ::File.basename(file.path) + elsif file.respond_to?(:name) + file.name + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data_part.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data_part.rb new file mode 100644 index 000000000000..de45416ee087 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_form_data_part.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "securerandom" + +module Seed + module Internal + module Multipart + # @api private + class FormDataPart + attr_reader :name, :contents, :filename, :headers + + # @param name [String] The name of the form field + # @param value [String, Integer, Float, Boolean, File, #read] The value of the field + # @param filename [String, nil] Optional filename for file uploads + # @param headers [Hash, nil] Optional additional headers + def initialize(name:, value:, filename: nil, headers: nil) + @name = name + @contents = convert_to_content(value) + @filename = filename + @headers = headers + end + + # Converts the part to a hash suitable for serialization. + # + # @return [Hash] A hash representation of the part + def to_hash + result = { + name: @name, + contents: @contents + } + result[:filename] = @filename if @filename + result[:headers] = @headers if @headers + result + end + + private + + # Converts various types of values to a content representation + # @param value [String, Integer, Float, Boolean, #read] The value to convert + # @return [String] The string representation of the value + def convert_to_content(value) + if value.respond_to?(:read) + value.read + else + value.to_s + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_request.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_request.rb new file mode 100644 index 000000000000..9fa80cee01ab --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/multipart/multipart_request.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Multipart + # @api private + class Request < Seed::Internal::Http::BaseRequest + attr_reader :body + + # @param base_url [String] The base URL for the request + # @param path [String] The path for the request + # @param method [Symbol] The HTTP method for the request (:get, :post, etc.) + # @param headers [Hash] Additional headers for the request (optional) + # @param query [Hash] Query parameters for the request (optional) + # @param body [MultipartFormData, nil] The multipart form data for the request (optional) + # @param request_options [Seed::RequestOptions, Hash{Symbol=>Object}, nil] + def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {}) + super(base_url:, path:, method:, headers:, query:, request_options:) + + @body = body + end + + # @return [Hash] The encoded HTTP request headers. + # @param protected_keys [Array] Header keys set by the SDK client (e.g. auth, metadata) + # that must not be overridden by additional_headers from request_options. + def encode_headers(protected_keys: []) + sdk_headers = { + "Content-Type" => @body.content_type + }.merge(@headers) + merge_additional_headers(sdk_headers, protected_keys:) + end + + # @return [String, nil] The encoded HTTP request body. + def encode_body + @body&.encode + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/array.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/array.rb new file mode 100644 index 000000000000..f3c7c1bd9549 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/array.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # An array of a specific type + class Array + include Seed::Internal::Types::Type + + attr_reader :type + + class << self + # Instantiates a new `Array` of a given type + # + # @param type [Object] The member type of this array + # + # @return [Seed::Internal::Types::Array] + def [](type) + new(type) + end + end + + # @api private + def initialize(type) + @type = type + end + + # Coerces a value into this array + # + # @param value [Object] + # @option strict [Boolean] + # @return [::Array] + def coerce(value, strict: strict?) + unless value.is_a?(::Array) + raise Errors::TypeError, "cannot coerce `#{value.class}` to Array<#{type}>" if strict + + return value + end + + value.map do |element| + Utils.coerce(type, element, strict: strict) + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/boolean.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/boolean.rb new file mode 100644 index 000000000000..d4e3277e566f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/boolean.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + module Boolean + extend Seed::Internal::Types::Union + + member TrueClass + member FalseClass + + # Overrides the base coercion method for enums to allow integer and string values to become booleans + # + # @param value [Object] + # @option strict [Boolean] + # @return [Object] + def self.coerce(value, strict: strict?) + case value + when TrueClass, FalseClass + return value + when Integer + return value == 1 + when String + return %w[1 true].include?(value) + end + + raise Errors::TypeError, "cannot coerce `#{value.class}` to Boolean" if strict + + value + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/enum.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/enum.rb new file mode 100644 index 000000000000..72e45e4c1f27 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/enum.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Module for defining enums + module Enum + include Type + + # @api private + # + # @return [Array] + def values + @values ||= constants.map { |c| const_get(c) } + end + + # @api private + def finalize! + values + end + + # @api private + def strict? + @strict ||= false + end + + # @api private + def strict! + @strict = true + end + + def coerce(value, strict: strict?) + coerced_value = Utils.coerce(Symbol, value) + + return coerced_value if values.include?(coerced_value) + + raise Errors::TypeError, "`#{value}` not in enum #{self}" if strict + + value + end + + # Parse JSON string and coerce to the enum value + # + # @param str [String] JSON string to parse + # @return [String] The enum value + def load(str) + coerce(::JSON.parse(str)) + end + + def inspect + "#{name}[#{values.join(", ")}]" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/hash.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/hash.rb new file mode 100644 index 000000000000..d8bffa63ac11 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/hash.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + class Hash + include Type + + attr_reader :key_type, :value_type + + class << self + def [](key_type, value_type) + new(key_type, value_type) + end + end + + def initialize(key_type, value_type) + @key_type = key_type + @value_type = value_type + end + + def coerce(value, strict: strict?) + unless value.is_a?(::Hash) + raise Errors::TypeError, "not hash" if strict + + return value + end + + value.to_h do |k, v| + [Utils.coerce(key_type, k, strict: strict), Utils.coerce(value_type, v, strict: strict)] + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model.rb new file mode 100644 index 000000000000..8caca14ff7ea --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # @abstract + # + # An abstract model that all data objects will inherit from + class Model + include Type + + class << self + # The defined fields for this model + # + # @api private + # + # @return [Hash] + def fields + @fields ||= if self < Seed::Internal::Types::Model + superclass.fields.dup + else + {} + end + end + + # Any extra fields that have been created from instantiation + # + # @api private + # + # @return [Hash] + def extra_fields + @extra_fields ||= {} + end + + # Define a new field on this model + # + # @param name [Symbol] The name of the field + # @param type [Class] Type of the field + # @option optional [Boolean] If it is an optional field + # @option nullable [Boolean] If it is a nullable field + # @option api_name [Symbol, String] Name in the API of this field. When serializing/deserializing, will use + # this field name + # @return [void] + def field(name, type, optional: false, nullable: false, api_name: nil, default: nil) + add_field_definition(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name, + default: default) + + define_accessor(name) + define_setter(name) + end + + # Define a new literal for this model + # + # @param name [Symbol] + # @param value [Object] + # @option api_name [Symbol, String] + # @return [void] + def literal(name, value, api_name: nil) + add_field_definition(name: name, type: value.class, optional: false, nullable: false, api_name: api_name, + value: value) + + define_accessor(name) + end + + # Adds a new field definition into the class's fields registry + # + # @api private + # + # @param name [Symbol] + # @param type [Class] + # @option optional [Boolean] + # @return [void] + private def add_field_definition(name:, type:, optional:, nullable:, api_name:, default: nil, value: nil) + fields[name.to_sym] = + Field.new(name: name, type: type, optional: optional, nullable: nullable, api_name: api_name, + value: value, default: default) + end + + # Adds a new field definition into the class's extra fields registry + # + # @api private + # + # @param name [Symbol] + # @param type [Class] + # @option required [Boolean] + # @option optional [Boolean] + # @return [void] + def add_extra_field_definition(name:, type:) + return if extra_fields.key?(name.to_sym) + + extra_fields[name.to_sym] = Field.new(name: name, type: type, optional: true, nullable: false) + + define_accessor(name) + define_setter(name) + end + + # @api private + private def define_accessor(name) + method_name = name.to_sym + + define_method(method_name) do + @data[name] + end + end + + # @api private + private def define_setter(name) + method_name = :"#{name}=" + + define_method(method_name) do |val| + @data[name] = val + end + end + + def coerce(value, strict: (respond_to?(:strict?) ? strict? : false)) # rubocop:disable Lint/UnusedMethodArgument + return value if value.is_a?(self) + + return value unless value.is_a?(::Hash) + + new(value) + end + + def load(str) + coerce(::JSON.parse(str, symbolize_names: true)) + end + + def ===(instance) + instance.class.ancestors.include?(self) + end + end + + # Creates a new instance of this model + # TODO: Should all this logic be in `#coerce` instead? + # + # @param values [Hash] + # @option strict [Boolean] + # @return [self] + def initialize(values = {}) + @data = {} + + values = Utils.symbolize_keys(values.dup) + + self.class.fields.each do |field_name, field| + value = values.delete(field.api_name.to_sym) || values.delete(field.api_name) || values.delete(field_name) + + field_value = value || (if field.literal? + field.value + elsif field.default + field.default + end) + + @data[field_name] = Utils.coerce(field.type, field_value) + end + + # Any remaining values in the input become extra fields + values.each do |name, value| + self.class.add_extra_field_definition(name: name, type: value.class) + + @data[name.to_sym] = value + end + end + + def to_h + result = self.class.fields.merge(self.class.extra_fields).each_with_object({}) do |(name, field), acc| + # If there is a value present in the data, use that value + # If there is a `nil` value present in the data, and it is optional but NOT nullable, exclude key altogether + # If there is a `nil` value present in the data, and it is optional and nullable, use the nil value + + value = @data[name] + + next if value.nil? && field.optional && !field.nullable + + if value.is_a?(::Array) + value = value.map { |item| item.respond_to?(:to_h) ? item.to_h : item } + elsif value.respond_to?(:to_h) + value = value.to_h + end + + acc[field.api_name] = value + end + + # Inject union discriminant if this instance was coerced from a discriminated union + # and the discriminant key is not already present in the result + discriminant_key = instance_variable_get(:@_fern_union_discriminant_key) + discriminant_value = instance_variable_get(:@_fern_union_discriminant_value) + result[discriminant_key] = discriminant_value if discriminant_key && discriminant_value && !result.key?(discriminant_key) + + result + end + + def ==(other) + self.class == other.class && to_h == other.to_h + end + + # @return [String] + def inspect + attrs = @data.map do |name, value| + field = self.class.fields[name] || self.class.extra_fields[name] + display_value = field&.sensitive? ? "[REDACTED]" : value.inspect + "#{name}=#{display_value}" + end + + "#<#{self.class.name}:0x#{object_id&.to_s(16)} #{attrs.join(" ")}>" + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model/field.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model/field.rb new file mode 100644 index 000000000000..6ce0186f6a5d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/model/field.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + class Model + # Definition of a field on a model + class Field + SENSITIVE_FIELD_NAMES = %i[ + password secret token api_key apikey access_token refresh_token + client_secret client_id credential bearer authorization + ].freeze + + attr_reader :name, :type, :optional, :nullable, :api_name, :value, :default + + def initialize(name:, type:, optional: false, nullable: false, api_name: nil, value: nil, default: nil) + @name = name.to_sym + @type = type + @optional = optional + @nullable = nullable + @api_name = api_name || name.to_s + @value = value + @default = default + end + + def literal? + !value.nil? + end + + def sensitive? + SENSITIVE_FIELD_NAMES.include?(@name) || + SENSITIVE_FIELD_NAMES.any? { |sensitive| @name.to_s.include?(sensitive.to_s) } + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/type.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/type.rb new file mode 100644 index 000000000000..5866caf1dbda --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/type.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # @abstract + module Type + include Seed::Internal::JSON::Serializable + + # Coerces a value to this type + # + # @param value [unknown] + # @option strict [Boolean] If we should strictly coerce this value + def coerce(value, strict: strict?) + raise NotImplementedError + end + + # Returns if strictness is on for this type, defaults to `false` + # + # @return [Boolean] + def strict? + @strict ||= false + end + + # Enable strictness by default for this type + # + # @return [void] + def strict! + @strict = true + self + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/union.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/union.rb new file mode 100644 index 000000000000..f3e118a2fa78 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/union.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Define a union between two types + module Union + include Seed::Internal::Types::Type + + def members + @members ||= [] + end + + # Add a member to this union + # + # @param type [Object] + # @option key [Symbol, String] + # @return [void] + def member(type, key: nil) + members.push([key, Utils.wrap_type(type)]) + self + end + + def type_member?(type) + members.any? { |_key, type_fn| type == type_fn.call } + end + + # Set the discriminant for this union + # + # @param key [Symbol, String] + # @return [void] + def discriminant(key) + @discriminant = key + end + + # @api private + private def discriminated? + !@discriminant.nil? + end + + # Check if value matches a type, handling type wrapper instances + # (Internal::Types::Hash and Internal::Types::Array instances) + # + # @param value [Object] + # @param member_type [Object] + # @return [Boolean] + private def type_matches?(value, member_type) + case member_type + when Seed::Internal::Types::Hash + value.is_a?(::Hash) + when Seed::Internal::Types::Array + value.is_a?(::Array) + when Class, Module + value.is_a?(member_type) + else + false + end + end + + # Resolves the type of a value to be one of the members + # + # @param value [Object] + # @return [Class] + private def resolve_member(value) + if discriminated? && value.is_a?(::Hash) + # Try both symbol and string keys for the discriminant + discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil) + + return if discriminant_value.nil? + + # Convert to string for consistent comparison + discriminant_str = discriminant_value.to_s + + # First try exact match + members_hash = members.to_h + result = members_hash[discriminant_str]&.call + return result if result + + # Try case-insensitive match as fallback + discriminant_lower = discriminant_str.downcase + matching_keys = members_hash.keys.select { |k| k.to_s.downcase == discriminant_lower } + + # Only use case-insensitive match if exactly one key matches (avoid ambiguity) + return members_hash[matching_keys.first]&.call if matching_keys.length == 1 + + nil + else + # First try exact type matching + result = members.find do |_key, mem| + member_type = Utils.unwrap_type(mem) + type_matches?(value, member_type) + end&.last&.call + + return result if result + + # For Hash values, try to coerce into Model member types + if value.is_a?(::Hash) + members.find do |_key, mem| + member_type = Utils.unwrap_type(mem) + # Check if member_type is a Model class + next unless member_type.is_a?(Class) && member_type <= Model + + # Try to coerce the hash into this model type with strict mode + begin + candidate = Utils.coerce(member_type, value, strict: true) + + # Validate that all required (non-optional) fields are present + # This ensures undiscriminated unions properly distinguish between member types + member_type.fields.each do |field_name, field| + raise Errors::TypeError, "Required field `#{field_name}` missing for union member #{member_type.name}" if candidate.instance_variable_get(:@data)[field_name].nil? && !field.optional + end + + true + rescue Errors::TypeError + false + end + end&.last&.call + end + end + end + + def coerce(value, strict: strict?) + type = resolve_member(value) + + unless type + return value unless strict + + if discriminated? + raise Errors::TypeError, + "value of type `#{value.class}` not member of union #{self}" + end + + raise Errors::TypeError, "could not resolve to member of union #{self}" + end + + coerced = Utils.coerce(type, value, strict: strict) + + # For discriminated unions, store the discriminant info on the coerced instance + # so it can be injected back during serialization (to_h) + if discriminated? && value.is_a?(::Hash) && coerced.is_a?(Model) + discriminant_value = value.fetch(@discriminant, nil) || value.fetch(@discriminant.to_s, nil) + if discriminant_value + coerced.instance_variable_set(:@_fern_union_discriminant_key, @discriminant.to_s) + coerced.instance_variable_set(:@_fern_union_discriminant_value, discriminant_value) + end + end + + coerced + end + + # Parse JSON string and coerce to the correct union member type + # + # @param str [String] JSON string to parse + # @return [Object] Coerced value matching a union member + def load(str) + coerce(::JSON.parse(str, symbolize_names: true)) + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/unknown.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/unknown.rb new file mode 100644 index 000000000000..7b58de956da9 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/unknown.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + module Unknown + include Seed::Internal::Types::Type + + def coerce(value) + value + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/utils.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/utils.rb new file mode 100644 index 000000000000..5a6eeb23b1b0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/internal/types/utils.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Seed + module Internal + module Types + # Utilities for dealing with and checking types + module Utils + # Wraps a type into a type function + # + # @param type [Proc, Object] + # @return [Proc] + def self.wrap_type(type) + case type + when Proc + type + else + -> { type } + end + end + + # Resolves a type or type function into a type + # + # @param type [Proc, Object] + # @return [Object] + def self.unwrap_type(type) + type.is_a?(Proc) ? type.call : type + end + + def self.coerce(target, value, strict: false) + type = unwrap_type(target) + + case type + in Array + case value + when ::Array + return type.coerce(value, strict: strict) + when Set, ::Hash + return coerce(type, value.to_a) + end + in Hash + case value + when ::Hash + return type.coerce(value, strict: strict) + when ::Array + return coerce(type, value.to_h) + end + in ->(t) { t <= NilClass } + return nil + in ->(t) { t <= String } + case value + when String, Symbol, Numeric, TrueClass, FalseClass + return value.to_s + end + in ->(t) { t <= Symbol } + case value + when Symbol, String + return value.to_sym + end + in ->(t) { t <= Integer } + case value + when Numeric, String, Time + return value.to_i + end + in ->(t) { t <= Float } + case value + when Numeric, Time, String + return value.to_f + end + in ->(t) { t <= Model } + case value + when type + return value + when ::Hash + return type.coerce(value, strict: strict) + end + in Module + case type + in ->(t) { + t.singleton_class.include?(Enum) || + t.singleton_class.include?(Union) + } + return type.coerce(value, strict: strict) + else + value # rubocop:disable Lint/Void + end + else + value # rubocop:disable Lint/Void + end + + raise Errors::TypeError, "cannot coerce value of type `#{value.class}` to `#{target}`" if strict + + value + end + + def self.symbolize_keys(hash) + hash.transform_keys(&:to_sym) + end + + # Converts camelCase keys to snake_case symbols + # This allows SDK methods to accept both snake_case and camelCase keys + # e.g., { refundMethod: ... } becomes { refund_method: ... } + # + # @param hash [Hash] + # @return [Hash] + def self.normalize_keys(hash) + hash.transform_keys do |key| + key_str = key.to_s + # Convert camelCase to snake_case + snake_case = key_str.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase + snake_case.to_sym + end + end + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/version.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/version.rb new file mode 100644 index 000000000000..00dd45cdd958 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/lib/seed/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Seed + VERSION = "0.0.1" +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/reference.md b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/reference.md new file mode 100644 index 000000000000..7bfdfbd7ac04 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/reference.md @@ -0,0 +1,118 @@ +# Reference +## BasicAuth +
client.basic_auth.get_with_basic_auth() -> Internal::Types::Boolean +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```ruby +client.basic_auth.get_with_basic_auth +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `Seed::BasicAuth::RequestOptions` + +
+
+
+
+ + +
+
+
+ +
client.basic_auth.post_with_basic_auth(request) -> Internal::Types::Boolean +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```ruby +client.basic_auth.post_with_basic_auth +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `Object` + +
+
+ +
+
+ +**request_options:** `Seed::BasicAuth::RequestOptions` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/seed.gemspec b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/seed.gemspec new file mode 100644 index 000000000000..6fdd2f7d837d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/seed.gemspec @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "lib/seed/version" +require_relative "custom.gemspec" + +# NOTE: A handful of these fields are required as part of the Ruby specification. +# You can change them here or overwrite them in the custom gemspec file. +Gem::Specification.new do |spec| + spec.name = "fern_basic-auth-pw-omitted" + spec.authors = ["Seed"] + spec.version = Seed::VERSION + spec.summary = "Ruby client library for the Seed API" + spec.description = "The Seed Ruby library provides convenient access to the Seed API from Ruby." + spec.required_ruby_version = ">= 3.3.0" + spec.metadata["rubygems_mfa_required"] = "true" + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + gemspec = File.basename(__FILE__) + spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| + ls.readlines("\x0", chomp: true).reject do |f| + (f == gemspec) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) + end + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.add_dependency "base64" + # For more information and examples about making a new gem, check out our + # guide at: https://bundler.io/guides/creating_gem.html + + # Load custom gemspec configuration if it exists + custom_gemspec_file = File.join(__dir__, "custom.gemspec.rb") + add_custom_gemspec_data(spec) if File.exist?(custom_gemspec_file) +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/snippet.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/snippet.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/custom.test.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/custom.test.rb new file mode 100644 index 000000000000..4bd57989d43d --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/custom.test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# This is a custom test file, if you wish to add more tests +# to your SDK. +# Be sure to mark this file in `.fernignore`. +# +# If you include example requests/responses in your fern definition, +# you will have tests automatically generated for you. + +# This test is run via command line: rake customtest +describe "Custom Test" do + it "Default" do + refute false + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/test_helper.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/test_helper.rb new file mode 100644 index 000000000000..b086fe6d76ec --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/test_helper.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "../lib/seed" diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_cursor_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_cursor_item_iterator.rb new file mode 100644 index 000000000000..44f85cb20b35 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_cursor_item_iterator.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "stringio" +require "json" +require "test_helper" + +NUMBERS = (1..65).to_a +PageResponse = Struct.new(:cards, :next_cursor) + +class CursorItemIteratorTest < Minitest::Test + def make_iterator(initial_cursor:) + @times_called = 0 + + Seed::Internal::CursorItemIterator.new(initial_cursor:, cursor_field: :next_cursor, item_field: :cards) do |cursor| + @times_called += 1 + cursor ||= 0 + next_cursor = cursor + 10 + PageResponse.new( + cards: NUMBERS[cursor...next_cursor], + next_cursor: next_cursor < NUMBERS.length ? next_cursor : nil + ) + end + end + + def test_item_iterator_can_iterate_to_exhaustion + iterator = make_iterator(initial_cursor: 0) + + assert_equal NUMBERS, iterator.to_a + assert_equal 7, @times_called + + iterator = make_iterator(initial_cursor: 10) + + assert_equal (11..65).to_a, iterator.to_a + + iterator = make_iterator(initial_cursor: 5) + + assert_equal (6..65).to_a, iterator.to_a + end + + def test_item_iterator_can_work_without_an_initial_cursor + iterator = make_iterator(initial_cursor: nil) + + assert_equal NUMBERS, iterator.to_a + assert_equal 7, @times_called + end + + def test_items_iterator_iterates_lazily + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + assert_equal 1, iterator.first + assert_equal 1, @times_called + + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + assert_equal (1..15).to_a, iterator.first(15) + assert_equal 2, @times_called + + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + iterator.each do |card| + break if card >= 15 + end + + assert_equal 2, @times_called + end + + def test_items_iterator_implements_enumerable + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + doubled = iterator.map { |card| card * 2 } + + assert_equal 7, @times_called + assert_equal NUMBERS.length, doubled.length + end + + def test_items_iterator_can_be_advanced_manually + iterator = make_iterator(initial_cursor: 0) + + assert_equal 0, @times_called + + items = [] + expected_times_called = 0 + while (item = iterator.next_element) + expected_times_called += 1 if (item % 10) == 1 + + assert_equal expected_times_called, @times_called + assert_equal item != NUMBERS.last, iterator.next?, "#{item} #{iterator}" + items.push(item) + end + + assert_equal 7, @times_called + assert_equal NUMBERS, items + end + + def test_pages_iterator + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal( + [ + (1..10).to_a, + (11..20).to_a, + (21..30).to_a, + (31..40).to_a, + (41..50).to_a, + (51..60).to_a, + (61..65).to_a + ], + iterator.to_a.map(&:cards) + ) + + iterator = make_iterator(initial_cursor: 10).pages + + assert_equal( + [ + (11..20).to_a, + (21..30).to_a, + (31..40).to_a, + (41..50).to_a, + (51..60).to_a, + (61..65).to_a + ], + iterator.to_a.map(&:cards) + ) + end + + def test_pages_iterator_can_work_without_an_initial_cursor + iterator = make_iterator(initial_cursor: nil).pages + + assert_equal 7, iterator.to_a.length + assert_equal 7, @times_called + end + + def test_pages_iterator_iterates_lazily + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + iterator.first + + assert_equal 1, @times_called + + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + assert_equal 2, iterator.first(2).length + assert_equal 2, @times_called + end + + def test_pages_iterator_knows_whether_another_page_is_upcoming + iterator = make_iterator(initial_cursor: 0).pages + + iterator.each_with_index do |_page, index| + assert_equal index + 1, @times_called + assert_equal index < 6, iterator.next? + end + end + + def test_pages_iterator_can_be_advanced_manually + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + + lengths = [] + expected_times_called = 0 + while (page = iterator.next_page) + expected_times_called += 1 + + assert_equal expected_times_called, @times_called + lengths.push(page.cards.length) + end + + assert_equal 7, @times_called + assert_equal [10, 10, 10, 10, 10, 10, 5], lengths + end + + def test_pages_iterator_implements_enumerable + iterator = make_iterator(initial_cursor: 0).pages + + assert_equal 0, @times_called + lengths = iterator.map { |page| page.cards.length } + + assert_equal 7, @times_called + assert_equal [10, 10, 10, 10, 10, 10, 5], lengths + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_offset_item_iterator.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_offset_item_iterator.rb new file mode 100644 index 000000000000..004f394f0a41 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/iterators/test_offset_item_iterator.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "stringio" +require "json" +require "test_helper" + +OffsetPageResponse = Struct.new(:items, :has_next) +TestIteratorConfig = Struct.new( + :step, + :has_next_field, + :total_item_count, + :per_page, + :initial_page +) do + def first_item_returned + if step + (initial_page || 0) + 1 + else + (((initial_page || 1) - 1) * per_page) + 1 + end + end +end + +LAZY_TEST_ITERATOR_CONFIG = TestIteratorConfig.new(initial_page: 1, step: false, has_next_field: :has_next, total_item_count: 65, per_page: 10) +ALL_TEST_ITERATOR_CONFIGS = [true, false].map do |step| + [:has_next, nil].map do |has_next_field| + [0, 5, 10, 60, 63].map do |total_item_count| + [5, 10].map do |per_page| + initial_pages = [nil, 3, 100] + initial_pages << (step ? 0 : 1) + + initial_pages.map do |initial_page| + TestIteratorConfig.new( + step: step, + has_next_field: has_next_field, + total_item_count: total_item_count, + per_page: per_page, + initial_page: initial_page + ) + end + end + end + end +end.flatten + +class OffsetItemIteratorTest < Minitest::Test + def make_iterator(config) + @times_called = 0 + + items = (1..config.total_item_count).to_a + + Seed::Internal::OffsetItemIterator.new( + initial_page: config.initial_page, + item_field: :items, + has_next_field: config.has_next_field, + step: config.step + ) do |page| + @times_called += 1 + + slice_start = config.step ? page : (page - 1) * config.per_page + slice_end = slice_start + config.per_page + + output = { + items: items[slice_start...slice_end] + } + output[config.has_next_field] = slice_end < items.length if config.has_next_field + + OffsetPageResponse.new(**output) + end + end + + def test_item_iterator_can_iterate_to_exhaustion + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config) + + assert_equal (config.first_item_returned..config.total_item_count).to_a, iterator.to_a + end + end + + def test_items_iterator_can_be_advanced_manually_and_has_accurate_has_next + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config) + items = [] + + while (item = iterator.next_element) + assert_equal(item != config.total_item_count, iterator.next?, "#{item} #{iterator}") + items.push(item) + end + + assert_equal (config.first_item_returned..config.total_item_count).to_a, items + end + end + + def test_pages_iterator_can_be_advanced_manually_and_has_accurate_has_next + ALL_TEST_ITERATOR_CONFIGS.each do |config| + iterator = make_iterator(config).pages + pages = [] + + loop do + has_next_output = iterator.next? + page = iterator.next_page + + assert_equal(has_next_output, !page.nil?, "next? was inaccurate: #{config} #{iterator.inspect}") + break if page.nil? + + pages.push(page) + end + + assert_equal pages, make_iterator(config).pages.to_a + end + end + + def test_items_iterator_iterates_lazily + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + assert_equal 1, iterator.first + assert_equal 1, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + assert_equal (1..15).to_a, iterator.first(15) + assert_equal 2, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG) + + assert_equal 0, @times_called + iterator.each do |card| + break if card >= 15 + end + + assert_equal 2, @times_called + end + + def test_pages_iterator_iterates_lazily + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG).pages + + assert_equal 0, @times_called + iterator.first + + assert_equal 1, @times_called + + iterator = make_iterator(LAZY_TEST_ITERATOR_CONFIG).pages + + assert_equal 0, @times_called + assert_equal 3, iterator.first(3).length + assert_equal 3, @times_called + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_array.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_array.rb new file mode 100644 index 000000000000..e7e6571f03ee --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_array.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Array do + module TestArray + StringArray = Seed::Internal::Types::Array[String] + end + + describe "#initialize" do + it "sets the type" do + assert_equal String, TestArray::StringArray.type + end + end + + describe "#coerce" do + it "does not perform coercion if not an array" do + assert_equal 1, TestArray::StringArray.coerce(1) + end + + it "raises an error if not an array and strictness is on" do + assert_raises Seed::Internal::Errors::TypeError do + TestArray::StringArray.coerce(1, strict: true) + end + end + + it "coerces the elements" do + assert_equal %w[foobar 1 true], TestArray::StringArray.coerce(["foobar", 1, true]) + end + + it "raises an error if element of array is not coercable and strictness is on" do + assert_raises Seed::Internal::Errors::TypeError do + TestArray::StringArray.coerce([Object.new], strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_boolean.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_boolean.rb new file mode 100644 index 000000000000..cba18e48765b --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_boolean.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Boolean do + describe ".coerce" do + it "coerces true/false" do + assert Seed::Internal::Types::Boolean.coerce(true) + refute Seed::Internal::Types::Boolean.coerce(false) + end + + it "coerces an Integer" do + assert Seed::Internal::Types::Boolean.coerce(1) + refute Seed::Internal::Types::Boolean.coerce(0) + end + + it "coerces a String" do + assert Seed::Internal::Types::Boolean.coerce("1") + assert Seed::Internal::Types::Boolean.coerce("true") + refute Seed::Internal::Types::Boolean.coerce("0") + end + + it "passes through other values with strictness off" do + obj = Object.new + + assert_equal obj, Seed::Internal::Types::Boolean.coerce(obj) + end + + it "raises an error with other values with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + Seed::Internal::Types::Boolean.coerce(Object.new, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_enum.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_enum.rb new file mode 100644 index 000000000000..e8d89bce467f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_enum.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Enum do + module EnumTest + module ExampleEnum + extend Seed::Internal::Types::Enum + + FOO = :foo + BAR = :bar + + finalize! + end + end + + describe "#values" do + it "defines values" do + assert_equal %i[foo bar].sort, EnumTest::ExampleEnum.values.sort + end + end + + describe "#coerce" do + it "coerces an existing member" do + assert_equal :foo, EnumTest::ExampleEnum.coerce(:foo) + end + + it "coerces a string version of a member" do + assert_equal :foo, EnumTest::ExampleEnum.coerce("foo") + end + + it "returns the value if not a member with strictness off" do + assert_equal 1, EnumTest::ExampleEnum.coerce(1) + end + + it "raises an error if value is not a member with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + EnumTest::ExampleEnum.coerce(1, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_hash.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_hash.rb new file mode 100644 index 000000000000..6c5e58a6a946 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_hash.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Hash do + module TestHash + SymbolStringHash = Seed::Internal::Types::Hash[Symbol, String] + end + + describe ".[]" do + it "defines the key and value type" do + assert_equal Symbol, TestHash::SymbolStringHash.key_type + assert_equal String, TestHash::SymbolStringHash.value_type + end + end + + describe "#coerce" do + it "coerces the keys" do + assert_equal %i[foo bar], TestHash::SymbolStringHash.coerce({ "foo" => "1", :bar => "2" }).keys + end + + it "coerces the values" do + assert_equal %w[foo 1], TestHash::SymbolStringHash.coerce({ foo: :foo, bar: 1 }).values + end + + it "passes through other values with strictness off" do + obj = Object.new + + assert_equal obj, TestHash::SymbolStringHash.coerce(obj) + end + + it "raises an error with other values with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce(Object.new, strict: true) + end + end + + it "raises an error with non-coercable key types with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce({ Object.new => 1 }, strict: true) + end + end + + it "raises an error with non-coercable value types with strictness on" do + assert_raises Seed::Internal::Errors::TypeError do + TestHash::SymbolStringHash.coerce({ "foobar" => Object.new }, strict: true) + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_model.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_model.rb new file mode 100644 index 000000000000..3d87b9f5a8c7 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_model.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Model do + module StringInteger + extend Seed::Internal::Types::Union + + member String + member Integer + end + + class ExampleModel < Seed::Internal::Types::Model + field :name, String + field :rating, StringInteger, optional: true + field :year, Integer, optional: true, nullable: true, api_name: "yearOfRelease" + end + + class ExampleModelInheritance < ExampleModel + field :director, String + end + + class ExampleWithDefaults < ExampleModel + field :type, String, default: "example" + end + + class ExampleChild < Seed::Internal::Types::Model + field :value, String + end + + class ExampleParent < Seed::Internal::Types::Model + field :child, ExampleChild + end + + describe ".field" do + before do + @example = ExampleModel.new(name: "Inception", rating: 4) + end + + it "defines fields on model" do + assert_equal %i[name rating year], ExampleModel.fields.keys + end + + it "defines fields from parent models" do + assert_equal %i[name rating year director], ExampleModelInheritance.fields.keys + end + + it "sets the field's type" do + assert_equal String, ExampleModel.fields[:name].type + assert_equal StringInteger, ExampleModel.fields[:rating].type + end + + it "sets the `default` option" do + assert_equal "example", ExampleWithDefaults.fields[:type].default + end + + it "defines getters" do + assert_respond_to @example, :name + assert_respond_to @example, :rating + + assert_equal "Inception", @example.name + assert_equal 4, @example.rating + end + + it "defines setters" do + assert_respond_to @example, :name= + assert_respond_to @example, :rating= + + @example.name = "Inception 2" + @example.rating = 5 + + assert_equal "Inception 2", @example.name + assert_equal 5, @example.rating + end + end + + describe "#initialize" do + it "sets the data" do + example = ExampleModel.new(name: "Inception", rating: 4) + + assert_equal "Inception", example.name + assert_equal 4, example.rating + end + + it "allows extra fields to be set" do + example = ExampleModel.new(name: "Inception", rating: 4, director: "Christopher Nolan") + + assert_equal "Christopher Nolan", example.director + end + + it "sets the defaults where applicable" do + example_using_defaults = ExampleWithDefaults.new + + assert_equal "example", example_using_defaults.type + + example_without_defaults = ExampleWithDefaults.new(type: "not example") + + assert_equal "not example", example_without_defaults.type + end + + it "coerces child models" do + parent = ExampleParent.new(child: { value: "foobar" }) + + assert_kind_of ExampleChild, parent.child + end + + it "uses the api_name to pull the value" do + example = ExampleModel.new({ name: "Inception", yearOfRelease: 2014 }) + + assert_equal 2014, example.year + refute_respond_to example, :yearOfRelease + end + end + + describe "#inspect" do + class SensitiveModel < Seed::Internal::Types::Model + field :username, String + field :password, String + field :client_secret, String + field :access_token, String + field :api_key, String + end + + it "redacts sensitive fields" do + model = SensitiveModel.new( + username: "user123", + password: "secret123", + client_secret: "cs_abc", + access_token: "token_xyz", + api_key: "key_123" + ) + + inspect_output = model.inspect + + assert_includes inspect_output, "username=\"user123\"" + assert_includes inspect_output, "password=[REDACTED]" + assert_includes inspect_output, "client_secret=[REDACTED]" + assert_includes inspect_output, "access_token=[REDACTED]" + assert_includes inspect_output, "api_key=[REDACTED]" + refute_includes inspect_output, "secret123" + refute_includes inspect_output, "cs_abc" + refute_includes inspect_output, "token_xyz" + refute_includes inspect_output, "key_123" + end + + it "does not redact non-sensitive fields" do + example = ExampleModel.new(name: "Inception", rating: 4) + inspect_output = example.inspect + + assert_includes inspect_output, "name=\"Inception\"" + assert_includes inspect_output, "rating=4" + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_union.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_union.rb new file mode 100644 index 000000000000..e4e95c93139f --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_union.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Union do + class Rectangle < Seed::Internal::Types::Model + literal :type, "square" + + field :area, Float + end + + class Circle < Seed::Internal::Types::Model + literal :type, "circle" + + field :area, Float + end + + class Pineapple < Seed::Internal::Types::Model + literal :type, "pineapple" + + field :area, Float + end + + module Shape + extend Seed::Internal::Types::Union + + discriminant :type + + member -> { Rectangle }, key: "rect" + member -> { Circle }, key: "circle" + end + + module StringOrInteger + extend Seed::Internal::Types::Union + + member String + member Integer + end + + describe "#coerce" do + it "coerces hashes into member models with discriminated unions" do + circle = Shape.coerce({ type: "circle", area: 4.0 }) + + assert_instance_of Circle, circle + end + end + + describe "#type_member?" do + it "defines Model members" do + assert Shape.type_member?(Rectangle) + assert Shape.type_member?(Circle) + refute Shape.type_member?(Pineapple) + end + + it "defines other members" do + assert StringOrInteger.type_member?(String) + assert StringOrInteger.type_member?(Integer) + refute StringOrInteger.type_member?(Float) + refute StringOrInteger.type_member?(Pineapple) + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_utils.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_utils.rb new file mode 100644 index 000000000000..29d14621a229 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/unit/internal/types/test_utils.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Seed::Internal::Types::Utils do + Utils = Seed::Internal::Types::Utils + + module TestUtils + class M < Seed::Internal::Types::Model + field :value, String + end + + class UnionMemberA < Seed::Internal::Types::Model + literal :type, "A" + field :only_on_a, String + end + + class UnionMemberB < Seed::Internal::Types::Model + literal :type, "B" + field :only_on_b, String + end + + module U + extend Seed::Internal::Types::Union + + discriminant :type + + member -> { UnionMemberA }, key: "A" + member -> { UnionMemberB }, key: "B" + end + + SymbolStringHash = Seed::Internal::Types::Hash[Symbol, String] + SymbolModelHash = -> { Seed::Internal::Types::Hash[Symbol, TestUtils::M] } + end + + describe ".coerce" do + describe "NilClass" do + it "always returns nil" do + assert_nil Utils.coerce(NilClass, "foobar") + assert_nil Utils.coerce(NilClass, 1) + assert_nil Utils.coerce(NilClass, Object.new) + end + end + + describe "String" do + it "coerces from String, Symbol, Numeric, or Boolean" do + assert_equal "foobar", Utils.coerce(String, "foobar") + assert_equal "foobar", Utils.coerce(String, :foobar) + assert_equal "1", Utils.coerce(String, 1) + assert_equal "1.0", Utils.coerce(String, 1.0) + assert_equal "true", Utils.coerce(String, true) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(String, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(String, Object.new, strict: true) + end + end + end + + describe "Symbol" do + it "coerces from Symbol, String" do + assert_equal :foobar, Utils.coerce(Symbol, :foobar) + assert_equal :foobar, Utils.coerce(Symbol, "foobar") + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Symbol, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Symbol, Object.new, strict: true) + end + end + end + + describe "Integer" do + it "coerces from Numeric, String, Time" do + assert_equal 1, Utils.coerce(Integer, 1) + assert_equal 1, Utils.coerce(Integer, 1.0) + assert_equal 1, Utils.coerce(Integer, Complex.rect(1)) + assert_equal 1, Utils.coerce(Integer, Rational(1)) + assert_equal 1, Utils.coerce(Integer, "1") + assert_equal 1_713_916_800, Utils.coerce(Integer, Time.utc(2024, 4, 24)) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Integer, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Integer, Object.new, strict: true) + end + end + end + + describe "Float" do + it "coerces from Numeric, Time" do + assert_in_delta(1.0, Utils.coerce(Float, 1.0)) + assert_in_delta(1.0, Utils.coerce(Float, 1)) + assert_in_delta(1.0, Utils.coerce(Float, Complex.rect(1))) + assert_in_delta(1.0, Utils.coerce(Float, Rational(1))) + assert_in_delta(1_713_916_800.0, Utils.coerce(Integer, Time.utc(2024, 4, 24))) + end + + it "passes through value if it cannot be coerced and not strict" do + obj = Object.new + + assert_equal obj, Utils.coerce(Float, obj) + end + + it "raises an error if value cannot be coerced and strict" do + assert_raises Seed::Internal::Errors::TypeError do + Utils.coerce(Float, Object.new, strict: true) + end + end + end + + describe "Model" do + it "coerces a hash" do + result = Utils.coerce(TestUtils::M, { value: "foobar" }) + + assert_kind_of TestUtils::M, result + assert_equal "foobar", result.value + end + + it "coerces a hash when the target is a type function" do + result = Utils.coerce(-> { TestUtils::M }, { value: "foobar" }) + + assert_kind_of TestUtils::M, result + assert_equal "foobar", result.value + end + + it "will not coerce non-hashes" do + assert_equal "foobar", Utils.coerce(TestUtils::M, "foobar") + end + end + + describe "Enum" do + module ExampleEnum + extend Seed::Internal::Types::Enum + + FOO = :FOO + BAR = :BAR + + finalize! + end + + it "coerces into a Symbol version of the member value" do + assert_equal :FOO, Utils.coerce(ExampleEnum, "FOO") + end + + it "returns given value if not a member" do + assert_equal "NOPE", Utils.coerce(ExampleEnum, "NOPE") + end + end + + describe "Array" do + StringArray = Seed::Internal::Types::Array[String] + ModelArray = -> { Seed::Internal::Types::Array[TestUtils::M] } + UnionArray = -> { Seed::Internal::Types::Array[TestUtils::U] } + + it "coerces an array of literals" do + assert_equal %w[a b c], Utils.coerce(StringArray, %w[a b c]) + assert_equal ["1", "2.0", "true"], Utils.coerce(StringArray, [1, 2.0, true]) + assert_equal ["1", "2.0", "true"], Utils.coerce(StringArray, Set.new([1, 2.0, true])) + end + + it "coerces an array of Models" do + assert_equal [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")], + Utils.coerce(ModelArray, [{ value: "foobar" }, { value: "bizbaz" }]) + + assert_equal [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")], + Utils.coerce(ModelArray, [TestUtils::M.new(value: "foobar"), TestUtils::M.new(value: "bizbaz")]) + end + + it "coerces an array of model unions" do + assert_equal [TestUtils::UnionMemberA.new(type: "A", only_on_a: "A"), TestUtils::UnionMemberB.new(type: "B", only_on_b: "B")], + Utils.coerce(UnionArray, [{ type: "A", only_on_a: "A" }, { type: "B", only_on_b: "B" }]) + end + + it "returns given value if not an array" do + assert_equal 1, Utils.coerce(StringArray, 1) + end + end + + describe "Hash" do + it "coerces the keys and values" do + ssh_res = Utils.coerce(TestUtils::SymbolStringHash, { "foo" => "bar", "biz" => "2" }) + + assert_equal "bar", ssh_res[:foo] + assert_equal "2", ssh_res[:biz] + + smh_res = Utils.coerce(TestUtils::SymbolModelHash, { "foo" => { "value" => "foo" } }) + + assert_equal TestUtils::M.new(value: "foo"), smh_res[:foo] + end + end + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb new file mode 100644 index 000000000000..22d1d388694c --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative "wiremock_test_case" + +class BasicAuthWireTest < WireMockTestCase + def setup + super + + @client = Seed::Client.new( + username: "test-username", + base_url: WIREMOCK_BASE_URL + ) + end + + def test_basic_auth_get_with_basic_auth_with_wiremock + test_id = "basic_auth.get_with_basic_auth.0" + + @client.basic_auth.get_with_basic_auth(request_options: { + additional_headers: { + "X-Test-Id" => "basic_auth.get_with_basic_auth.0" + } + }) + + verify_request_count( + test_id: test_id, + method: "GET", + url_path: "/basic-auth", + query_params: nil, + expected: 1 + ) + end + + def test_basic_auth_post_with_basic_auth_with_wiremock + test_id = "basic_auth.post_with_basic_auth.0" + + @client.basic_auth.post_with_basic_auth(request_options: { + additional_headers: { + "X-Test-Id" => "basic_auth.post_with_basic_auth.0" + } + }) + + verify_request_count( + test_id: test_id, + method: "POST", + url_path: "/basic-auth", + query_params: nil, + expected: 1 + ) + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wire_helper.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wire_helper.rb new file mode 100644 index 000000000000..ce0ea46b0406 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wire_helper.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "test_helper" + +# WireMock container lifecycle management for wire tests. +# It automatically starts the WireMock container before tests and stops it after. +# If WIREMOCK_URL is already set (external orchestration), container management is skipped. + +WIREMOCK_COMPOSE_FILE = File.expand_path("../../wiremock/docker-compose.test.yml", __dir__) + +# Start WireMock container when this file is required +if ENV["RUN_WIRE_TESTS"] == "true" && File.exist?(WIREMOCK_COMPOSE_FILE) && !ENV["WIREMOCK_URL"] + puts "Starting WireMock container..." + warn "Failed to start WireMock container" unless system("docker compose -f #{WIREMOCK_COMPOSE_FILE} up -d --wait") + + # Discover the dynamically assigned port and set WIREMOCK_URL + port_output = `docker compose -f #{WIREMOCK_COMPOSE_FILE} port wiremock 8080 2>&1`.strip + if port_output =~ /:(\d+)$/ + ENV["WIREMOCK_URL"] = "http://localhost:#{Regexp.last_match(1)}" + puts "WireMock container is ready at #{ENV.fetch("WIREMOCK_URL", nil)}" + else + ENV["WIREMOCK_URL"] = "http://localhost:8080" + puts "WireMock container is ready (default port 8080)" + end + + # Stop WireMock container after all tests complete + Minitest.after_run do + puts "Stopping WireMock container..." + system("docker compose -f #{WIREMOCK_COMPOSE_FILE} down") + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb new file mode 100644 index 000000000000..26de2f33e8e4 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "wire_helper" +require "net/http" +require "json" +require "uri" +require "seed" + +# Base test case for WireMock-based wire tests. +# +# This class provides helper methods for verifying requests made to WireMock +# and manages the test lifecycle for integration tests. +class WireMockTestCase < Minitest::Test + WIREMOCK_BASE_URL = ENV["WIREMOCK_URL"] || "http://localhost:8080" + WIREMOCK_ADMIN_URL = "#{WIREMOCK_BASE_URL}/__admin".freeze + + def setup + super + return if ENV["RUN_WIRE_TESTS"] == "true" + + skip "Wire tests are disabled by default. Set RUN_WIRE_TESTS=true to enable them." + end + + # Verifies the number of requests made to WireMock filtered by test ID for concurrency safety. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param query_params [Hash, nil] Query parameters to match + # @param expected [Integer] Expected number of requests + def verify_request_count(test_id:, method:, url_path:, expected:, query_params: nil) + admin_url = ENV["WIREMOCK_URL"] ? "#{ENV["WIREMOCK_URL"]}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + request_body["queryParameters"] = query_params.transform_values { |v| { "equalTo" => v } } if query_params + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" + end +end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/docker-compose.test.yml b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/docker-compose.test.yml new file mode 100644 index 000000000000..58747d54a46b --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/docker-compose.test.yml @@ -0,0 +1,14 @@ +services: + wiremock: + image: wiremock/wiremock:3.9.1 + ports: + - "0:8080" # Use dynamic port to avoid conflicts with concurrent tests + volumes: + - ./wiremock-mappings.json:/home/wiremock/mappings/wiremock-mappings.json + command: ["--global-response-templating", "--verbose"] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 5s diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json new file mode 100644 index 000000000000..e4105fe17be0 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json @@ -0,0 +1,60 @@ +{ + "mappings": [ + { + "id": "ce59c023-78fc-4d8d-8e8c-95f5e1a6204a", + "name": "getWithBasicAuth - default", + "request": { + "urlPathTemplate": "/basic-auth", + "method": "GET" + }, + "response": { + "status": 200, + "body": "true", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "ce59c023-78fc-4d8d-8e8c-95f5e1a6204a", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "9cf6385c-29ea-4710-8792-fd2e00adc7c7", + "name": "postWithBasicAuth - default", + "request": { + "urlPathTemplate": "/basic-auth", + "method": "POST" + }, + "response": { + "status": 200, + "body": "true", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "9cf6385c-29ea-4710-8792-fd2e00adc7c7", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + } + ], + "meta": { + "total": 2 + } +} \ No newline at end of file diff --git a/seed/ruby-sdk-v2/seed.yml b/seed/ruby-sdk-v2/seed.yml index a503260d8792..c45a14e033e2 100644 --- a/seed/ruby-sdk-v2/seed.yml +++ b/seed/ruby-sdk-v2/seed.yml @@ -49,6 +49,10 @@ fixtures: - outputFolder: wire-tests customConfig: enableWireTests: true + basic-auth-pw-omitted: + - outputFolder: wire-tests + customConfig: + enableWireTests: true exhaustive: - outputFolder: wire-tests customConfig: From 6a023f3497d2dd4fc8c8f566434e155264699a7d Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:15:11 +0000 Subject: [PATCH 18/20] feat(ruby-sdk): add Authorization header assertion to wire tests - Add verify_authorization_header helper to WireMockTestCase base class - Wire tests now assert the exact Authorization header value on captured requests - basic-auth-pw-omitted: asserts 'Basic base64(test-username:)' (empty password) - basic-auth: asserts 'Basic base64(test-username:test-password)' (both fields) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/src/wire-tests/WireTestGenerator.ts | 30 +++++++++++++++++++ .../src/wire-tests/WireTestSetupGenerator.ts | 25 ++++++++++++++++ .../wire-tests/test/wire/basic_auth_test.rb | 14 +++++++++ .../test/wire/wiremock_test_case.rb | 26 ++++++++++++++++ .../basic-auth/wire-tests/.fern/metadata.json | 2 +- .../basic-auth/wire-tests/seed.gemspec | 1 - .../wire-tests/test/wire/basic_auth_test.rb | 14 +++++++++ .../test/wire/wiremock_test_case.rb | 26 ++++++++++++++++ 8 files changed, 136 insertions(+), 2 deletions(-) diff --git a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts index 47bc8c46758f..16e6b80a1f11 100644 --- a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts +++ b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts @@ -352,6 +352,19 @@ export class WireTestGenerator { lines.push(` query_params: ${queryParamsCode},`); lines.push(` expected: 1`); lines.push(` )`); + + // Verify Authorization header when basic auth is configured + const expectedAuthHeader = this.buildExpectedAuthorizationHeader(); + if (expectedAuthHeader != null) { + lines.push(``); + lines.push(` verify_authorization_header(`); + lines.push(` test_id: test_id,`); + lines.push(` method: "${endpoint.method}",`); + lines.push(` url_path: "${basePath}",`); + lines.push(` expected_value: "${expectedAuthHeader}"`); + lines.push(` )`); + } + lines.push(" end"); return lines; @@ -536,6 +549,23 @@ export class WireTestGenerator { return out; } + /** + * Builds the expected Authorization header value for basic auth. + * Returns the full header value (e.g., "Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk") + * or null if no basic auth scheme is configured. + */ + private buildExpectedAuthorizationHeader(): string | null { + for (const scheme of this.context.ir.auth.schemes) { + if (scheme.type === "basic") { + const username = scheme.usernameOmit ? "" : "test-username"; + const password = scheme.passwordOmit ? "" : "test-password"; + const encoded = Buffer.from(`${username}:${password}`).toString("base64"); + return `Basic ${encoded}`; + } + } + return null; + } + private toPascalCase(str: string): string { return str .split("_") diff --git a/generators/ruby-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts b/generators/ruby-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts index 7e7a539c193f..ec684039508f 100644 --- a/generators/ruby-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts +++ b/generators/ruby-v2/sdk/src/wire-tests/WireTestSetupGenerator.ts @@ -298,6 +298,31 @@ class WireMockTestCase < Minitest::Test assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" end + + # Verifies that the Authorization header on captured requests matches the expected value. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param expected_value [String] The expected Authorization header value + def verify_authorization_header(test_id:, method:, url_path:, expected_value:) + admin_url = ENV['WIREMOCK_URL'] ? "#{ENV['WIREMOCK_URL']}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + refute_empty requests, "No requests found for test_id #{test_id}" + actual_header = requests.first.dig("request", "headers", "Authorization") + assert_equal expected_value, actual_header, "Expected Authorization header '#{expected_value}', got '#{actual_header}'" + end end `; } diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb index 22d1d388694c..46391d6843ae 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb @@ -28,6 +28,13 @@ def test_basic_auth_get_with_basic_auth_with_wiremock query_params: nil, expected: 1 ) + + verify_authorization_header( + test_id: test_id, + method: "GET", + url_path: "/basic-auth", + expected_value: "Basic dGVzdC11c2VybmFtZTo=" + ) end def test_basic_auth_post_with_basic_auth_with_wiremock @@ -46,5 +53,12 @@ def test_basic_auth_post_with_basic_auth_with_wiremock query_params: nil, expected: 1 ) + + verify_authorization_header( + test_id: test_id, + method: "POST", + url_path: "/basic-auth", + expected_value: "Basic dGVzdC11c2VybmFtZTo=" + ) end end diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb index 26de2f33e8e4..dec2e7e529b4 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb @@ -46,4 +46,30 @@ def verify_request_count(test_id:, method:, url_path:, expected:, query_params: assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" end + + # Verifies that the Authorization header on captured requests matches the expected value. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param expected_value [String] The expected Authorization header value + def verify_authorization_header(test_id:, method:, url_path:, expected_value:) + admin_url = ENV["WIREMOCK_URL"] ? "#{ENV["WIREMOCK_URL"]}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + refute_empty requests, "No requests found for test_id #{test_id}" + actual_header = requests.first.dig("request", "headers", "Authorization") + + assert_equal expected_value, actual_header, "Expected Authorization header '#{expected_value}', got '#{actual_header}'" + end end diff --git a/seed/ruby-sdk-v2/basic-auth/wire-tests/.fern/metadata.json b/seed/ruby-sdk-v2/basic-auth/wire-tests/.fern/metadata.json index cd5e87482aab..c22a966f974c 100644 --- a/seed/ruby-sdk-v2/basic-auth/wire-tests/.fern/metadata.json +++ b/seed/ruby-sdk-v2/basic-auth/wire-tests/.fern/metadata.json @@ -1,7 +1,7 @@ { "cliVersion": "DUMMY", "generatorName": "fernapi/fern-ruby-sdk-v2", - "generatorVersion": "local", + "generatorVersion": "latest", "generatorConfig": { "enableWireTests": true }, diff --git a/seed/ruby-sdk-v2/basic-auth/wire-tests/seed.gemspec b/seed/ruby-sdk-v2/basic-auth/wire-tests/seed.gemspec index 8a6bc34c67ff..1b3bd323ed32 100644 --- a/seed/ruby-sdk-v2/basic-auth/wire-tests/seed.gemspec +++ b/seed/ruby-sdk-v2/basic-auth/wire-tests/seed.gemspec @@ -26,7 +26,6 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "base64" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/basic_auth_test.rb b/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/basic_auth_test.rb index c4934ed5a2d9..00cc66467aa7 100644 --- a/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/basic_auth_test.rb +++ b/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/basic_auth_test.rb @@ -29,6 +29,13 @@ def test_basic_auth_get_with_basic_auth_with_wiremock query_params: nil, expected: 1 ) + + verify_authorization_header( + test_id: test_id, + method: "GET", + url_path: "/basic-auth", + expected_value: "Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk" + ) end def test_basic_auth_post_with_basic_auth_with_wiremock @@ -47,5 +54,12 @@ def test_basic_auth_post_with_basic_auth_with_wiremock query_params: nil, expected: 1 ) + + verify_authorization_header( + test_id: test_id, + method: "POST", + url_path: "/basic-auth", + expected_value: "Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk" + ) end end diff --git a/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/wiremock_test_case.rb b/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/wiremock_test_case.rb index 26de2f33e8e4..dec2e7e529b4 100644 --- a/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/wiremock_test_case.rb +++ b/seed/ruby-sdk-v2/basic-auth/wire-tests/test/wire/wiremock_test_case.rb @@ -46,4 +46,30 @@ def verify_request_count(test_id:, method:, url_path:, expected:, query_params: assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" end + + # Verifies that the Authorization header on captured requests matches the expected value. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param expected_value [String] The expected Authorization header value + def verify_authorization_header(test_id:, method:, url_path:, expected_value:) + admin_url = ENV["WIREMOCK_URL"] ? "#{ENV["WIREMOCK_URL"]}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + refute_empty requests, "No requests found for test_id #{test_id}" + actual_header = requests.first.dig("request", "headers", "Authorization") + + assert_equal expected_value, actual_header, "Expected Authorization header '#{expected_value}', got '#{actual_header}'" + end end From 77ee9f94453f09725de0415f3038bcbb294fc97e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:22:38 +0000 Subject: [PATCH 19/20] fix(ruby-sdk): skip auth header assertion when both username and password are omitted When both usernameOmit and passwordOmit are true, RootClientGenerator skips the Authorization header entirely. buildExpectedAuthorizationHeader now matches this behavior by returning null (no assertion) instead of 'Basic Og==' (base64(':')). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts index 16e6b80a1f11..32ca2850429d 100644 --- a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts +++ b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts @@ -557,6 +557,9 @@ export class WireTestGenerator { private buildExpectedAuthorizationHeader(): string | null { for (const scheme of this.context.ir.auth.schemes) { if (scheme.type === "basic") { + if (scheme.usernameOmit && scheme.passwordOmit) { + continue; + } const username = scheme.usernameOmit ? "" : "test-username"; const password = scheme.passwordOmit ? "" : "test-password"; const encoded = Buffer.from(`${username}:${password}`).toString("base64"); From 889e958c5789795edbf9d6f80712aaa2586ef92a Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:50:51 +0000 Subject: [PATCH 20/20] feat(mock-utils): add exact Authorization header matching to WireMock mappings - Basic auth: uses equalTo with exact base64-encoded credentials - Bearer auth: uses matches with 'Bearer .+' pattern - Header auth: uses matches with '.+' pattern - Handles usernameOmit/passwordOmit for basic auth (omitted field = empty string) - Regenerated seed output for Ruby (basic-auth, basic-auth-pw-omitted, exhaustive, examples) and Rust (exhaustive, examples, simple-api) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/commons/mock-utils/index.ts | 27 +- .../wiremock/wiremock-mappings.json | 14 +- .../wiremock/wiremock-mappings.json | 14 +- .../test/wire/wiremock_test_case.rb | 26 ++ .../wiremock/wiremock-mappings.json | 24 +- .../test/wire/wiremock_test_case.rb | 26 ++ .../wiremock/wiremock-mappings.json | 352 ++++++++++++++++-- .../wiremock/wiremock-mappings.json | 24 +- .../wiremock/wiremock-mappings.json | 352 ++++++++++++++++-- .../wiremock/wiremock-mappings.json | 5 + 10 files changed, 766 insertions(+), 98 deletions(-) diff --git a/packages/commons/mock-utils/index.ts b/packages/commons/mock-utils/index.ts index 102ff53efe86..7c88bd036a03 100644 --- a/packages/commons/mock-utils/index.ts +++ b/packages/commons/mock-utils/index.ts @@ -15,7 +15,7 @@ export interface WireMockMapping { request: { urlPathTemplate: string; method: string; - headers?: Record; + headers?: Record; pathParameters?: Record; queryParameters?: Record; formParameters?: Record; @@ -267,16 +267,27 @@ export class WireMock { const shouldAddBodyPattern = needsBodyPattern && isSseResponse; // Build auth header matchers for endpoints that require authentication. - // Skip auth header matching when endpoint has per-endpoint security because - // the client configures all auth schemes globally and header overwriting - // (e.g., multiple schemes writing to "Authorization") makes the exact value unpredictable. - const authHeaders: Record = {}; - if (endpoint.auth && !(endpoint.security != null && endpoint.security.length > 0)) { + const authHeaders: Record = {}; + if (endpoint.auth) { for (const scheme of ir.auth.schemes) { switch (scheme.type) { - case "basic": - authHeaders["Authorization"] = { matches: "Basic .+" }; + case "basic": { + // Compute exact Authorization header using test credentials. + // Access usernameOmit/passwordOmit via runtime property check + // (available in IR v63+ but @fern-fern/ir-sdk types may lag). + const schemeRecord = scheme as unknown as Record; + const usernameOmit = schemeRecord.usernameOmit === true; + const passwordOmit = schemeRecord.passwordOmit === true; + if (usernameOmit && passwordOmit) { + // Both omitted — SDK skips the Authorization header entirely. + break; + } + const username = usernameOmit ? "" : "test-username"; + const password = passwordOmit ? "" : "test-password"; + const encoded = Buffer.from(`${username}:${password}`).toString("base64"); + authHeaders["Authorization"] = { equalTo: `Basic ${encoded}` }; break; + } case "bearer": authHeaders["Authorization"] = { matches: "Bearer .+" }; break; diff --git a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json index e4105fe17be0..8a3e7dd565af 100644 --- a/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json @@ -5,7 +5,12 @@ "name": "getWithBasicAuth - default", "request": { "urlPathTemplate": "/basic-auth", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTo=" + } + } }, "response": { "status": 200, @@ -32,7 +37,12 @@ "name": "postWithBasicAuth - default", "request": { "urlPathTemplate": "/basic-auth", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTo=" + } + } }, "response": { "status": 200, diff --git a/seed/ruby-sdk-v2/basic-auth/wire-tests/wiremock/wiremock-mappings.json b/seed/ruby-sdk-v2/basic-auth/wire-tests/wiremock/wiremock-mappings.json index e4105fe17be0..043be0044335 100644 --- a/seed/ruby-sdk-v2/basic-auth/wire-tests/wiremock/wiremock-mappings.json +++ b/seed/ruby-sdk-v2/basic-auth/wire-tests/wiremock/wiremock-mappings.json @@ -5,7 +5,12 @@ "name": "getWithBasicAuth - default", "request": { "urlPathTemplate": "/basic-auth", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk" + } + } }, "response": { "status": 200, @@ -32,7 +37,12 @@ "name": "postWithBasicAuth - default", "request": { "urlPathTemplate": "/basic-auth", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk" + } + } }, "response": { "status": 200, diff --git a/seed/ruby-sdk-v2/examples/wire-tests/test/wire/wiremock_test_case.rb b/seed/ruby-sdk-v2/examples/wire-tests/test/wire/wiremock_test_case.rb index 26de2f33e8e4..dec2e7e529b4 100644 --- a/seed/ruby-sdk-v2/examples/wire-tests/test/wire/wiremock_test_case.rb +++ b/seed/ruby-sdk-v2/examples/wire-tests/test/wire/wiremock_test_case.rb @@ -46,4 +46,30 @@ def verify_request_count(test_id:, method:, url_path:, expected:, query_params: assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" end + + # Verifies that the Authorization header on captured requests matches the expected value. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param expected_value [String] The expected Authorization header value + def verify_authorization_header(test_id:, method:, url_path:, expected_value:) + admin_url = ENV["WIREMOCK_URL"] ? "#{ENV["WIREMOCK_URL"]}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + refute_empty requests, "No requests found for test_id #{test_id}" + actual_header = requests.first.dig("request", "headers", "Authorization") + + assert_equal expected_value, actual_header, "Expected Authorization header '#{expected_value}', got '#{actual_header}'" + end end diff --git a/seed/ruby-sdk-v2/examples/wire-tests/wiremock/wiremock-mappings.json b/seed/ruby-sdk-v2/examples/wire-tests/wiremock/wiremock-mappings.json index 39a85251985f..69296f5b1908 100644 --- a/seed/ruby-sdk-v2/examples/wire-tests/wiremock/wiremock-mappings.json +++ b/seed/ruby-sdk-v2/examples/wire-tests/wiremock/wiremock-mappings.json @@ -57,7 +57,12 @@ "name": "getException - default", "request": { "urlPathTemplate": "/file/notification/{notificationId}", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -85,6 +90,11 @@ "request": { "urlPathTemplate": "/file/{filename}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "filename": { "equalTo": "file.txt" @@ -116,6 +126,11 @@ "request": { "urlPathTemplate": "/check/{id}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id-2sdx82h" @@ -146,7 +161,12 @@ "name": "ping - Example0", "request": { "urlPathTemplate": "/ping", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, diff --git a/seed/ruby-sdk-v2/exhaustive/wire-tests/test/wire/wiremock_test_case.rb b/seed/ruby-sdk-v2/exhaustive/wire-tests/test/wire/wiremock_test_case.rb index 26de2f33e8e4..dec2e7e529b4 100644 --- a/seed/ruby-sdk-v2/exhaustive/wire-tests/test/wire/wiremock_test_case.rb +++ b/seed/ruby-sdk-v2/exhaustive/wire-tests/test/wire/wiremock_test_case.rb @@ -46,4 +46,30 @@ def verify_request_count(test_id:, method:, url_path:, expected:, query_params: assert_equal expected, requests.length, "Expected #{expected} requests, found #{requests.length}" end + + # Verifies that the Authorization header on captured requests matches the expected value. + # + # @param test_id [String] The test ID used to filter requests + # @param method [String] The HTTP method (GET, POST, etc.) + # @param url_path [String] The URL path to match + # @param expected_value [String] The expected Authorization header value + def verify_authorization_header(test_id:, method:, url_path:, expected_value:) + admin_url = ENV["WIREMOCK_URL"] ? "#{ENV["WIREMOCK_URL"]}/__admin" : WIREMOCK_ADMIN_URL + uri = URI("#{admin_url}/requests/find") + http = Net::HTTP.new(uri.host, uri.port) + post_request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" }) + + request_body = { "method" => method, "urlPath" => url_path } + request_body["headers"] = { "X-Test-Id" => { "equalTo" => test_id } } + + post_request.body = request_body.to_json + response = http.request(post_request) + result = JSON.parse(response.body) + requests = result["requests"] || [] + + refute_empty requests, "No requests found for test_id #{test_id}" + actual_header = requests.first.dig("request", "headers", "Authorization") + + assert_equal expected_value, actual_header, "Expected Authorization header '#{expected_value}', got '#{actual_header}'" + end end diff --git a/seed/ruby-sdk-v2/exhaustive/wire-tests/wiremock/wiremock-mappings.json b/seed/ruby-sdk-v2/exhaustive/wire-tests/wiremock/wiremock-mappings.json index 09670fa261e5..a1128f12af7c 100644 --- a/seed/ruby-sdk-v2/exhaustive/wire-tests/wiremock/wiremock-mappings.json +++ b/seed/ruby-sdk-v2/exhaustive/wire-tests/wiremock/wiremock-mappings.json @@ -5,7 +5,12 @@ "name": "getAndReturnListOfPrimitives - default", "request": { "urlPathTemplate": "/container/list-of-primitives", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -31,7 +36,12 @@ "name": "getAndReturnListOfObjects - default", "request": { "urlPathTemplate": "/container/list-of-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -57,7 +67,12 @@ "name": "getAndReturnSetOfPrimitives - default", "request": { "urlPathTemplate": "/container/set-of-primitives", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -83,7 +98,12 @@ "name": "getAndReturnSetOfObjects - default", "request": { "urlPathTemplate": "/container/set-of-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -109,7 +129,12 @@ "name": "getAndReturnMapPrimToPrim - default", "request": { "urlPathTemplate": "/container/map-prim-to-prim", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -135,7 +160,12 @@ "name": "getAndReturnMapOfPrimToObject - default", "request": { "urlPathTemplate": "/container/map-prim-to-object", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -161,7 +191,12 @@ "name": "getAndReturnMapOfPrimToUndiscriminatedUnion - default", "request": { "urlPathTemplate": "/container/map-prim-to-union", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -187,7 +222,12 @@ "name": "getAndReturnOptional - default", "request": { "urlPathTemplate": "/container/opt-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -213,7 +253,12 @@ "name": "postJsonPatchContentType - default", "request": { "urlPathTemplate": "/foo/bar", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -239,7 +284,12 @@ "name": "postJsonPatchContentWithCharsetType - default", "request": { "urlPathTemplate": "/foo/baz", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -265,7 +315,12 @@ "name": "getAndReturnEnum - default", "request": { "urlPathTemplate": "/enum", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -292,6 +347,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -322,7 +382,12 @@ "name": "testPost - default", "request": { "urlPathTemplate": "/http-methods", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -349,6 +414,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -380,6 +450,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "PATCH", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -411,6 +486,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "DELETE", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -441,7 +521,12 @@ "name": "getAndReturnWithOptionalField - default", "request": { "urlPathTemplate": "/object/get-and-return-with-optional-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -467,7 +552,12 @@ "name": "getAndReturnWithRequiredField - default", "request": { "urlPathTemplate": "/object/get-and-return-with-required-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -493,7 +583,12 @@ "name": "getAndReturnWithMapOfMap - default", "request": { "urlPathTemplate": "/object/get-and-return-with-map-of-map", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -519,7 +614,12 @@ "name": "getAndReturnNestedWithOptionalField - default", "request": { "urlPathTemplate": "/object/get-and-return-nested-with-optional-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -546,6 +646,11 @@ "request": { "urlPathTemplate": "/object/get-and-return-nested-with-required-field/{string}", "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "string": { "equalTo": "string" @@ -576,7 +681,12 @@ "name": "getAndReturnNestedWithRequiredFieldAsList - default", "request": { "urlPathTemplate": "/object/get-and-return-nested-with-required-field-list", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -602,7 +712,12 @@ "name": "getAndReturnWithUnknownField - BackslashExample", "request": { "urlPathTemplate": "/object/get-and-return-with-unknown-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -628,7 +743,12 @@ "name": "getAndReturnWithDocumentedUnknownType - default", "request": { "urlPathTemplate": "/object/get-and-return-with-documented-unknown-type", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -654,7 +774,12 @@ "name": "getAndReturnMapOfDocumentedUnknownType - default", "request": { "urlPathTemplate": "/object/get-and-return-map-of-documented-unknown-type", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -680,7 +805,12 @@ "name": "getAndReturnWithDatetimeLikeString - DatetimeLikeStringExample", "request": { "urlPathTemplate": "/object/get-and-return-with-datetime-like-string", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -706,7 +836,12 @@ "name": "listItems - default", "request": { "urlPathTemplate": "/pagination", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -734,6 +869,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -765,6 +905,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -795,7 +940,12 @@ "name": "getWithQuery - default", "request": { "urlPathTemplate": "/params", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -822,7 +972,12 @@ "name": "getWithAllowMultipleQuery - default", "request": { "urlPathTemplate": "/params", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -850,6 +1005,11 @@ "request": { "urlPathTemplate": "/params/path-query/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -881,6 +1041,11 @@ "request": { "urlPathTemplate": "/params/path-query/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -912,6 +1077,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -943,6 +1113,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -974,6 +1149,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -1004,7 +1184,12 @@ "name": "getAndReturnString - default", "request": { "urlPathTemplate": "/primitive/string", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1030,7 +1215,12 @@ "name": "getAndReturnInt - default", "request": { "urlPathTemplate": "/primitive/integer", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1056,7 +1246,12 @@ "name": "getAndReturnLong - default", "request": { "urlPathTemplate": "/primitive/long", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1082,7 +1277,12 @@ "name": "getAndReturnDouble - default", "request": { "urlPathTemplate": "/primitive/double", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1108,7 +1308,12 @@ "name": "getAndReturnBool - default", "request": { "urlPathTemplate": "/primitive/boolean", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1134,7 +1339,12 @@ "name": "getAndReturnDatetime - default", "request": { "urlPathTemplate": "/primitive/datetime", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1160,7 +1370,12 @@ "name": "getAndReturnDate - default", "request": { "urlPathTemplate": "/primitive/date", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1186,7 +1401,12 @@ "name": "getAndReturnUUID - default", "request": { "urlPathTemplate": "/primitive/uuid", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1212,7 +1432,12 @@ "name": "getAndReturnBase64 - default", "request": { "urlPathTemplate": "/primitive/base64", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1239,6 +1464,11 @@ "request": { "urlPathTemplate": "/{id}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -1269,7 +1499,12 @@ "name": "getAndReturnUnion - default", "request": { "urlPathTemplate": "/union", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1295,7 +1530,12 @@ "name": "withMixedCase - default", "request": { "urlPathTemplate": "/urls/MixedCase", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1322,7 +1562,12 @@ "name": "noEndingSlash - default", "request": { "urlPathTemplate": "/urls/no-ending-slash", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1349,7 +1594,12 @@ "name": "withEndingSlash - default", "request": { "urlPathTemplate": "/urls/with-ending-slash/", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1376,7 +1626,12 @@ "name": "withUnderscores - default", "request": { "urlPathTemplate": "/urls/with_underscores", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1455,7 +1710,12 @@ "name": "getWithNoRequestBody - default", "request": { "urlPathTemplate": "/no-req-body", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1482,7 +1742,12 @@ "name": "postWithNoRequestBody - default", "request": { "urlPathTemplate": "/no-req-body", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1508,7 +1773,12 @@ "name": "getWithCustomHeader - default", "request": { "urlPathTemplate": "/test-headers/custom-header", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, diff --git a/seed/rust-sdk/examples/readme-config/wiremock/wiremock-mappings.json b/seed/rust-sdk/examples/readme-config/wiremock/wiremock-mappings.json index 2b7cfb35b885..876ec7e8480f 100644 --- a/seed/rust-sdk/examples/readme-config/wiremock/wiremock-mappings.json +++ b/seed/rust-sdk/examples/readme-config/wiremock/wiremock-mappings.json @@ -57,7 +57,12 @@ "name": "getException - default", "request": { "urlPathTemplate": "/file/notification/{notificationId}", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -85,6 +90,11 @@ "request": { "urlPathTemplate": "/file/{filename}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "filename": { "equalTo": "file.txt" @@ -116,6 +126,11 @@ "request": { "urlPathTemplate": "/check/{id}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id-2sdx82h" @@ -146,7 +161,12 @@ "name": "ping - Example0", "request": { "urlPathTemplate": "/ping", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, diff --git a/seed/rust-sdk/exhaustive/wiremock/wiremock-mappings.json b/seed/rust-sdk/exhaustive/wiremock/wiremock-mappings.json index aa9597896096..5940350c8ec1 100644 --- a/seed/rust-sdk/exhaustive/wiremock/wiremock-mappings.json +++ b/seed/rust-sdk/exhaustive/wiremock/wiremock-mappings.json @@ -5,7 +5,12 @@ "name": "getAndReturnListOfPrimitives - default", "request": { "urlPathTemplate": "/container/list-of-primitives", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -31,7 +36,12 @@ "name": "getAndReturnListOfObjects - default", "request": { "urlPathTemplate": "/container/list-of-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -57,7 +67,12 @@ "name": "getAndReturnSetOfPrimitives - default", "request": { "urlPathTemplate": "/container/set-of-primitives", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -83,7 +98,12 @@ "name": "getAndReturnSetOfObjects - default", "request": { "urlPathTemplate": "/container/set-of-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -109,7 +129,12 @@ "name": "getAndReturnMapPrimToPrim - default", "request": { "urlPathTemplate": "/container/map-prim-to-prim", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -135,7 +160,12 @@ "name": "getAndReturnMapOfPrimToObject - default", "request": { "urlPathTemplate": "/container/map-prim-to-object", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -161,7 +191,12 @@ "name": "getAndReturnMapOfPrimToUndiscriminatedUnion - default", "request": { "urlPathTemplate": "/container/map-prim-to-union", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -187,7 +222,12 @@ "name": "getAndReturnOptional - default", "request": { "urlPathTemplate": "/container/opt-objects", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -213,7 +253,12 @@ "name": "postJsonPatchContentType - default", "request": { "urlPathTemplate": "/foo/bar", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -239,7 +284,12 @@ "name": "postJsonPatchContentWithCharsetType - default", "request": { "urlPathTemplate": "/foo/baz", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -265,7 +315,12 @@ "name": "getAndReturnEnum - default", "request": { "urlPathTemplate": "/enum", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -292,6 +347,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -322,7 +382,12 @@ "name": "testPost - default", "request": { "urlPathTemplate": "/http-methods", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -349,6 +414,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -380,6 +450,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "PATCH", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -411,6 +486,11 @@ "request": { "urlPathTemplate": "/http-methods/{id}", "method": "DELETE", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -441,7 +521,12 @@ "name": "getAndReturnWithOptionalField - default", "request": { "urlPathTemplate": "/object/get-and-return-with-optional-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -467,7 +552,12 @@ "name": "getAndReturnWithRequiredField - default", "request": { "urlPathTemplate": "/object/get-and-return-with-required-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -493,7 +583,12 @@ "name": "getAndReturnWithMapOfMap - default", "request": { "urlPathTemplate": "/object/get-and-return-with-map-of-map", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -519,7 +614,12 @@ "name": "getAndReturnNestedWithOptionalField - default", "request": { "urlPathTemplate": "/object/get-and-return-nested-with-optional-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -546,6 +646,11 @@ "request": { "urlPathTemplate": "/object/get-and-return-nested-with-required-field/{string}", "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "string": { "equalTo": "string" @@ -576,7 +681,12 @@ "name": "getAndReturnNestedWithRequiredFieldAsList - default", "request": { "urlPathTemplate": "/object/get-and-return-nested-with-required-field-list", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -602,7 +712,12 @@ "name": "getAndReturnWithUnknownField - BackslashExample", "request": { "urlPathTemplate": "/object/get-and-return-with-unknown-field", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -628,7 +743,12 @@ "name": "getAndReturnWithDocumentedUnknownType - default", "request": { "urlPathTemplate": "/object/get-and-return-with-documented-unknown-type", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -654,7 +774,12 @@ "name": "getAndReturnMapOfDocumentedUnknownType - default", "request": { "urlPathTemplate": "/object/get-and-return-map-of-documented-unknown-type", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -680,7 +805,12 @@ "name": "getAndReturnWithDatetimeLikeString - DatetimeLikeStringExample", "request": { "urlPathTemplate": "/object/get-and-return-with-datetime-like-string", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -706,7 +836,12 @@ "name": "listItems - default", "request": { "urlPathTemplate": "/pagination", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -734,6 +869,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -765,6 +905,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -795,7 +940,12 @@ "name": "getWithQuery - default", "request": { "urlPathTemplate": "/params", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -822,7 +972,12 @@ "name": "getWithAllowMultipleQuery - default", "request": { "urlPathTemplate": "/params", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -850,6 +1005,11 @@ "request": { "urlPathTemplate": "/params/path-query/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -881,6 +1041,11 @@ "request": { "urlPathTemplate": "/params/path-query/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -912,6 +1077,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -943,6 +1113,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -974,6 +1149,11 @@ "request": { "urlPathTemplate": "/params/path/{param}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "param": { "equalTo": "param" @@ -1004,7 +1184,12 @@ "name": "getAndReturnString - default", "request": { "urlPathTemplate": "/primitive/string", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1030,7 +1215,12 @@ "name": "getAndReturnInt - default", "request": { "urlPathTemplate": "/primitive/integer", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1056,7 +1246,12 @@ "name": "getAndReturnLong - default", "request": { "urlPathTemplate": "/primitive/long", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1082,7 +1277,12 @@ "name": "getAndReturnDouble - default", "request": { "urlPathTemplate": "/primitive/double", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1108,7 +1308,12 @@ "name": "getAndReturnBool - default", "request": { "urlPathTemplate": "/primitive/boolean", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1134,7 +1339,12 @@ "name": "getAndReturnDatetime - default", "request": { "urlPathTemplate": "/primitive/datetime", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1160,7 +1370,12 @@ "name": "getAndReturnDate - default", "request": { "urlPathTemplate": "/primitive/date", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1186,7 +1401,12 @@ "name": "getAndReturnUUID - default", "request": { "urlPathTemplate": "/primitive/uuid", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1212,7 +1432,12 @@ "name": "getAndReturnBase64 - default", "request": { "urlPathTemplate": "/primitive/base64", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1239,6 +1464,11 @@ "request": { "urlPathTemplate": "/{id}", "method": "PUT", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id" @@ -1269,7 +1499,12 @@ "name": "getAndReturnUnion - default", "request": { "urlPathTemplate": "/union", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1295,7 +1530,12 @@ "name": "withMixedCase - default", "request": { "urlPathTemplate": "/urls/MixedCase", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1322,7 +1562,12 @@ "name": "noEndingSlash - default", "request": { "urlPathTemplate": "/urls/no-ending-slash", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1349,7 +1594,12 @@ "name": "withEndingSlash - default", "request": { "urlPathTemplate": "/urls/with-ending-slash/", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1376,7 +1626,12 @@ "name": "withUnderscores - default", "request": { "urlPathTemplate": "/urls/with_underscores", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1455,7 +1710,12 @@ "name": "getWithNoRequestBody - default", "request": { "urlPathTemplate": "/no-req-body", - "method": "GET" + "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1482,7 +1742,12 @@ "name": "postWithNoRequestBody - default", "request": { "urlPathTemplate": "/no-req-body", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, @@ -1508,7 +1773,12 @@ "name": "getWithCustomHeader - default", "request": { "urlPathTemplate": "/test-headers/custom-header", - "method": "POST" + "method": "POST", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + } }, "response": { "status": 200, diff --git a/seed/rust-sdk/simple-api/basic-custom-config/wiremock/wiremock-mappings.json b/seed/rust-sdk/simple-api/basic-custom-config/wiremock/wiremock-mappings.json index 60ad7d35b22b..9174d97706ce 100644 --- a/seed/rust-sdk/simple-api/basic-custom-config/wiremock/wiremock-mappings.json +++ b/seed/rust-sdk/simple-api/basic-custom-config/wiremock/wiremock-mappings.json @@ -6,6 +6,11 @@ "request": { "urlPathTemplate": "/users/{id}", "method": "GET", + "headers": { + "Authorization": { + "matches": "Bearer .+" + } + }, "pathParameters": { "id": { "equalTo": "id"