diff --git a/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index 9da7f1332655..c588817e6f33 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; + const passwordOmitted = !!authRecord.passwordOmit; + 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({ diff --git a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts index cd5e5f233399..688ff3160855 100644 --- a/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts +++ b/generators/ruby-v2/sdk/src/root-client/RootClientGenerator.ts @@ -116,6 +116,8 @@ export class RootClientGenerator extends FileGenerator 1) { - if (i === 0) { - writer.writeLine(`if !${usernameName}.nil? && !${passwordName}.nil?`); + if (isFirstBlock) { + writer.writeLine(`if ${condition}`); } else { - writer.writeLine(`elsif !${usernameName}.nil? && !${passwordName}.nil?`); + writer.writeLine(`elsif ${condition}`); } + isFirstBlock = false; + emittedAnyBlock = true; writer.writeLine( - ` headers["Authorization"] = "Basic #{Base64.strict_encode64("#{${usernameName}}:#{${passwordName}}")}"` + ` headers["Authorization"] = "Basic #{Base64.strict_encode64(${credentialStr})}"` ); - 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(${credentialStr})}"` ); } } + if (emittedAnyBlock && (isAuthOptional || basicAuthSchemes.length > 1)) { + writer.writeLine(`end`); + } } writer.write(`@raw_client = `); writer.writeNode(this.context.getRawClientClassReference()); @@ -344,30 +372,37 @@ export class RootClientGenerator extends FileGenerator { - writer.write(`ENV.fetch("${scheme.usernameEnvVar}", nil)`); - }) - : undefined, - docs: undefined - }); - parameters.push(usernameParam); - const passwordParam = ruby.parameters.keyword({ - name: this.case.snakeSafe(scheme.password), - type: ruby.Type.string(), - initializer: - scheme.passwordEnvVar != null - ? ruby.codeblock((writer) => { - writer.write(`ENV.fetch("${scheme.passwordEnvVar}", nil)`); - }) - : undefined, - docs: undefined - }); - parameters.push(passwordParam); + // When omit is true, the field is completely removed from the end-user API. + const usernameOmitted = !!scheme.usernameOmit; + const passwordOmitted = !!scheme.passwordOmit; + if (!usernameOmitted) { + const usernameParam = ruby.parameters.keyword({ + name: this.case.snakeSafe(scheme.username), + 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: this.case.snakeSafe(scheme.password), + 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": { diff --git a/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts b/generators/ruby-v2/sdk/src/wire-tests/WireTestGenerator.ts index fdf1cdea876e..32ca2850429d 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"'); @@ -348,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; @@ -532,6 +549,26 @@ 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") { + 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"); + 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/generators/ruby-v2/sdk/versions.yml b/generators/ruby-v2/sdk/versions.yml index 67a00470c335..9f71110757dc 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.4.0 + changelogEntry: + - summary: | + 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: 66 + - version: 1.3.0-rc.1 changelogEntry: - summary: | 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/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..aba6234f5885 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 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..46391d6843ae --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/basic_auth_test.rb @@ -0,0 +1,64 @@ +# 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 + ) + + 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 + 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 + ) + + 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/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..dec2e7e529b4 --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/test/wire/wiremock_test_case.rb @@ -0,0 +1,75 @@ +# 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 + + # 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/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..8a3e7dd565af --- /dev/null +++ b/seed/ruby-sdk-v2/basic-auth-pw-omitted/wire-tests/wiremock/wiremock-mappings.json @@ -0,0 +1,70 @@ +{ + "mappings": [ + { + "id": "ce59c023-78fc-4d8d-8e8c-95f5e1a6204a", + "name": "getWithBasicAuth - default", + "request": { + "urlPathTemplate": "/basic-auth", + "method": "GET", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTo=" + } + } + }, + "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", + "headers": { + "Authorization": { + "equalTo": "Basic dGVzdC11c2VybmFtZTo=" + } + } + }, + "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/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 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/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: 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"