From 4e02d95f9307fdc955c17ed4922b43c3e891ac50 Mon Sep 17 00:00:00 2001 From: cade Date: Sat, 11 Jul 2026 00:04:35 +0000 Subject: [PATCH 1/2] feat(java): read idempotency-key generation config from the IR Drive idempotency-key auto-generation from ir.getSdkConfig().getIdempotencyKeyGeneration() instead of a per-generator config flag. The header name and eligible HTTP methods come from the IR; a caller-supplied key always wins, otherwise a UUIDv4 is generated. Output is byte-identical when the feature is disabled. Depends on #17027 (IR v67 upgrade). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../unreleased/ir-driven-idempotency-key.yml | 8 + .../main/java/com/fern/java/client/Cli.java | 6 + .../client/ClientPoetClassNameFactory.java | 4 + .../generators/IdempotencyUtilsGenerator.java | 44 ++ .../endpoint/AbstractEndpointWriter.java | 77 ++- .../endpoint/NoRequestEndpointWriter.java | 15 +- .../endpoint/OnlyRequestEndpointWriter.java | 30 +- .../WrappedRequestEndpointWriter.java | 13 +- .../src/main/resources/IdempotencyUtils.java | 25 + .../src/checkVersionExists.ts | 29 +- .../.fern/metadata.json | 13 + .../.github/workflows/ci.yml | 0 .../.gitignore | 0 .../CONTRIBUTING.md | 0 .../README.md | 0 .../build.gradle | 0 .../reference.md | 0 .../sample-app/build.gradle | 0 .../sample-app/src/main/java/sample/App.java | 0 .../settings.gradle | 0 .../snippet.json | 0 .../AsyncSeedIdempotencyHeadersClient.java | 0 ...ncSeedIdempotencyHeadersClientBuilder.java | 0 .../SeedIdempotencyHeadersClient.java | 0 .../SeedIdempotencyHeadersClientBuilder.java | 0 .../core/ClientOptions.java | 0 .../core/ConsoleLogger.java | 0 .../core/DateTimeDeserializer.java | 0 .../core/DoubleSerializer.java | 0 .../idempotencyHeaders/core/Environment.java | 0 .../idempotencyHeaders/core/FileStream.java | 0 .../seed/idempotencyHeaders/core/ILogger.java | 0 .../core/IdempotencyUtils.java | 30 + .../core/IdempotentRequestOptions.java | 0 .../core/InputStreamRequestBody.java | 0 .../idempotencyHeaders/core/LogConfig.java | 0 .../idempotencyHeaders/core/LogLevel.java | 0 .../seed/idempotencyHeaders/core/Logger.java | 0 .../core/LoggingInterceptor.java | 0 .../idempotencyHeaders/core/MediaTypes.java | 0 .../idempotencyHeaders/core/Nullable.java | 0 .../core/NullableNonemptyFilter.java | 0 .../core/ObjectMappers.java | 0 .../core/QueryStringMapper.java | 0 .../core/RequestOptions.java | 0 .../core/ResponseBodyInputStream.java | 0 .../core/ResponseBodyReader.java | 0 .../ResponseDecompressionInterceptor.java | 0 .../core/RetryInterceptor.java | 0 .../core/Rfc2822DateTimeDeserializer.java | 0 .../SeedIdempotencyHeadersApiException.java | 0 .../core/SeedIdempotencyHeadersException.java | 0 .../SeedIdempotencyHeadersHttpResponse.java | 0 .../idempotencyHeaders/core/SseEvent.java | 0 .../core/SseEventParser.java | 0 .../seed/idempotencyHeaders/core/Stream.java | 0 .../idempotencyHeaders/core/Suppliers.java | 0 .../resources/payment/AsyncPaymentClient.java | 0 .../payment/AsyncRawPaymentClient.java | 178 ++++++ .../resources/payment/PaymentClient.java | 0 .../resources/payment/RawPaymentClient.java | 141 +++++ .../requests/CreatePaymentRequest.java | 0 .../resources/payment/types/Currency.java | 0 .../src/main/java/com/snippets/Example0.java | 0 .../src/main/java/com/snippets/Example1.java | 0 .../seed/idempotencyHeaders/StreamTest.java | 0 .../seed/idempotencyHeaders/TestClient.java | 0 .../core/QueryStringMapperTest.java | 0 .../.fern/metadata.json | 0 .../no-custom-config/.github/workflows/ci.yml | 65 +++ .../no-custom-config/.gitignore | 24 + .../no-custom-config/CONTRIBUTING.md | 114 ++++ .../no-custom-config/README.md | 229 ++++++++ .../no-custom-config/build.gradle | 102 ++++ .../no-custom-config/reference.md | 96 ++++ .../no-custom-config/sample-app/build.gradle | 19 + .../sample-app/src/main/java/sample/App.java | 13 + .../no-custom-config/settings.gradle | 3 + .../no-custom-config/snippet.json | 31 ++ .../AsyncSeedIdempotencyHeadersClient.java | 28 + ...ncSeedIdempotencyHeadersClientBuilder.java | 263 +++++++++ .../SeedIdempotencyHeadersClient.java | 28 + .../SeedIdempotencyHeadersClientBuilder.java | 263 +++++++++ .../core/ClientOptions.java | 310 +++++++++++ .../core/ConsoleLogger.java | 51 ++ .../core/DateTimeDeserializer.java | 70 +++ .../core/DoubleSerializer.java | 43 ++ .../idempotencyHeaders/core/Environment.java | 20 + .../idempotencyHeaders/core/FileStream.java | 60 ++ .../seed/idempotencyHeaders/core/ILogger.java | 38 ++ .../core/IdempotentRequestOptions.java | 184 +++++++ .../core/InputStreamRequestBody.java | 74 +++ .../idempotencyHeaders/core/LogConfig.java | 98 ++++ .../idempotencyHeaders/core/LogLevel.java | 36 ++ .../seed/idempotencyHeaders/core/Logger.java | 97 ++++ .../core/LoggingInterceptor.java | 104 ++++ .../idempotencyHeaders/core/MediaTypes.java | 13 + .../idempotencyHeaders/core/Nullable.java | 140 +++++ .../core/NullableNonemptyFilter.java | 22 + .../core/ObjectMappers.java | 46 ++ .../core/QueryStringMapper.java | 142 +++++ .../core/RequestOptions.java | 154 ++++++ .../core/ResponseBodyInputStream.java | 45 ++ .../core/ResponseBodyReader.java | 44 ++ .../ResponseDecompressionInterceptor.java | 56 ++ .../core/RetryInterceptor.java | 242 +++++++++ .../core/Rfc2822DateTimeDeserializer.java | 25 + .../SeedIdempotencyHeadersApiException.java | 73 +++ .../core/SeedIdempotencyHeadersException.java | 17 + .../SeedIdempotencyHeadersHttpResponse.java | 37 ++ .../idempotencyHeaders/core/SseEvent.java | 114 ++++ .../core/SseEventParser.java | 228 ++++++++ .../seed/idempotencyHeaders/core/Stream.java | 513 ++++++++++++++++++ .../idempotencyHeaders/core/Suppliers.java | 23 + .../resources/payment/AsyncPaymentClient.java | 45 ++ .../payment/AsyncRawPaymentClient.java | 0 .../resources/payment/PaymentClient.java | 44 ++ .../resources/payment/RawPaymentClient.java | 0 .../requests/CreatePaymentRequest.java | 141 +++++ .../resources/payment/types/Currency.java | 82 +++ .../src/main/java/com/snippets/Example0.java | 20 + .../src/main/java/com/snippets/Example1.java | 14 + .../seed/idempotencyHeaders/StreamTest.java | 120 ++++ .../seed/idempotencyHeaders/TestClient.java | 11 + .../core/QueryStringMapperTest.java | 339 ++++++++++++ .../.fern/metadata.json | 13 + .../.github/workflows/ci.yml | 65 +++ .../auto-generate-idempotency-key/.gitignore | 24 + .../CONTRIBUTING.md | 114 ++++ .../auto-generate-idempotency-key/README.md | 217 ++++++++ .../build.gradle | 102 ++++ .../reference.md | 2 + .../sample-app/build.gradle | 19 + .../sample-app/src/main/java/sample/App.java | 13 + .../settings.gradle | 3 + ...avaIdempotencyHeadersFileUploadClient.java | 28 + ...potencyHeadersFileUploadClientBuilder.java | 264 +++++++++ ...avaIdempotencyHeadersFileUploadClient.java | 28 + ...potencyHeadersFileUploadClientBuilder.java | 263 +++++++++ .../core/ClientOptions.java | 310 +++++++++++ .../core/ConsoleLogger.java | 51 ++ .../core/DateTimeDeserializer.java | 70 +++ .../core/DoubleSerializer.java | 43 ++ .../core/Environment.java | 20 + .../core/FileStream.java | 60 ++ .../core/ILogger.java | 38 ++ .../core/IdempotencyUtils.java | 30 + .../core/IdempotentRequestOptions.java | 184 +++++++ .../core/InputStreamRequestBody.java | 74 +++ .../core/LogConfig.java | 98 ++++ .../core/LogLevel.java | 36 ++ .../core/Logger.java | 97 ++++ .../core/LoggingInterceptor.java | 104 ++++ .../core/MediaTypes.java | 13 + .../core/Nullable.java | 140 +++++ .../core/NullableNonemptyFilter.java | 22 + .../core/ObjectMappers.java | 46 ++ .../core/QueryStringMapper.java | 142 +++++ .../core/RequestOptions.java | 154 ++++++ .../core/ResponseBodyInputStream.java | 45 ++ .../core/ResponseBodyReader.java | 44 ++ .../ResponseDecompressionInterceptor.java | 56 ++ .../core/RetryInterceptor.java | 242 +++++++++ .../core/Rfc2822DateTimeDeserializer.java | 25 + ...mpotencyHeadersFileUploadApiException.java | 74 +++ ...IdempotencyHeadersFileUploadException.java | 17 + ...mpotencyHeadersFileUploadHttpResponse.java | 37 ++ .../core/SseEvent.java | 114 ++++ .../core/SseEventParser.java | 228 ++++++++ .../core/Stream.java | 513 ++++++++++++++++++ .../core/Suppliers.java | 23 + .../service/AsyncRawServiceClient.java | 353 ++++++++++++ .../resources/service/AsyncServiceClient.java | 58 ++ .../resources/service/RawServiceClient.java | 272 ++++++++++ .../resources/service/ServiceClient.java | 56 ++ .../service/requests/UploadRequest.java | 105 ++++ .../StreamTest.java | 120 ++++ .../TestClient.java | 11 + .../core/QueryStringMapperTest.java | 339 ++++++++++++ seed/java-sdk/seed.yml | 9 + 180 files changed, 11414 insertions(+), 40 deletions(-) create mode 100644 generators/java/sdk/changes/unreleased/ir-driven-idempotency-key.yml create mode 100644 generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java create mode 100644 generators/java/sdk/src/main/resources/IdempotencyUtils.java create mode 100644 seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.fern/metadata.json rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/.github/workflows/ci.yml (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/.gitignore (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/CONTRIBUTING.md (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/README.md (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/build.gradle (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/reference.md (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/sample-app/build.gradle (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/sample-app/src/main/java/sample/App.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/settings.gradle (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/snippet.json (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Environment.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java (100%) create mode 100644 seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotencyUtils.java rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Logger.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Stream.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java (100%) create mode 100644 seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java (100%) create mode 100644 seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/snippets/Example0.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/main/java/com/snippets/Example1.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/test/java/com/seed/idempotencyHeaders/StreamTest.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/test/java/com/seed/idempotencyHeaders/TestClient.java (100%) rename seed/java-sdk/idempotency-headers/{ => auto-generate-idempotency-key}/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java (100%) rename seed/java-sdk/idempotency-headers/{ => no-custom-config}/.fern/metadata.json (100%) create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/.github/workflows/ci.yml create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/.gitignore create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/CONTRIBUTING.md create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/README.md create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/build.gradle create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/reference.md create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/sample-app/build.gradle create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/sample-app/src/main/java/sample/App.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/settings.gradle create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/snippet.json create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Environment.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Logger.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Stream.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java rename seed/java-sdk/idempotency-headers/{ => no-custom-config}/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java (100%) create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java rename seed/java-sdk/idempotency-headers/{ => no-custom-config}/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java (100%) create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example0.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example1.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/StreamTest.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/TestClient.java create mode 100644 seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.fern/metadata.json create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.github/workflows/ci.yml create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.gitignore create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/CONTRIBUTING.md create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/README.md create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/build.gradle create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/reference.md create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/build.gradle create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/settings.gradle create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClientBuilder.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ClientOptions.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ConsoleLogger.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DateTimeDeserializer.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DoubleSerializer.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Environment.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/FileStream.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ILogger.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotencyUtils.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotentRequestOptions.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/InputStreamRequestBody.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogConfig.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogLevel.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Logger.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LoggingInterceptor.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/MediaTypes.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Nullable.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/NullableNonemptyFilter.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ObjectMappers.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapper.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RequestOptions.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyInputStream.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyReader.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseDecompressionInterceptor.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RetryInterceptor.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Rfc2822DateTimeDeserializer.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadApiException.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadException.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadHttpResponse.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEvent.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEventParser.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Stream.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Suppliers.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncRawServiceClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncServiceClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/RawServiceClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/ServiceClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/requests/UploadRequest.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/StreamTest.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/TestClient.java create mode 100644 seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapperTest.java diff --git a/generators/java/sdk/changes/unreleased/ir-driven-idempotency-key.yml b/generators/java/sdk/changes/unreleased/ir-driven-idempotency-key.yml new file mode 100644 index 000000000000..6dc60b3eec07 --- /dev/null +++ b/generators/java/sdk/changes/unreleased/ir-driven-idempotency-key.yml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Idempotency-key auto-generation is now driven by the IR (`SdkConfig.idempotencyKeyGeneration`) + instead of a per-generator config flag. When enabled, the SDK adds the configured idempotency + header on the configured HTTP methods unless the caller already supplied one. The header name + and eligible methods come from the IR, and generated output is unchanged when the feature is off. + type: feat diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java b/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java index d627de578c13..feac75fb5be3 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/Cli.java @@ -40,6 +40,7 @@ import com.fern.java.client.generators.FileStreamGenerator; import com.fern.java.client.generators.HttpResponseGenerator; import com.fern.java.client.generators.ILoggerGenerator; +import com.fern.java.client.generators.IdempotencyUtilsGenerator; import com.fern.java.client.generators.InferredAuthTokenSupplierGenerator; import com.fern.java.client.generators.InputStreamRequestBodyGenerator; import com.fern.java.client.generators.LogConfigGenerator; @@ -383,6 +384,11 @@ public GeneratedRootClient generateClient( ResponseBodyReaderGenerator responseBodyReaderGenerator = new ResponseBodyReaderGenerator(context); this.addGeneratedFile(responseBodyReaderGenerator.generateFile()); + if (context.getIr().getSdkConfig().getIdempotencyKeyGeneration().isPresent()) { + IdempotencyUtilsGenerator idempotencyUtilsGenerator = new IdempotencyUtilsGenerator(context); + this.addGeneratedFile(idempotencyUtilsGenerator.generateFile()); + } + ClientOptionsGenerator clientOptionsGenerator = new ClientOptionsGenerator(context, generatedEnvironmentsClass, generatedRequestOptions); GeneratedClientOptions generatedClientOptions = clientOptionsGenerator.generateFile(); diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/ClientPoetClassNameFactory.java b/generators/java/sdk/src/main/java/com/fern/java/client/ClientPoetClassNameFactory.java index 0cbf657cb2e2..91e54ad635af 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/ClientPoetClassNameFactory.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/ClientPoetClassNameFactory.java @@ -113,6 +113,10 @@ public ClassName getMediaTypesClassName() { return ClassName.get(getCorePackage(), "MediaTypes"); } + public ClassName getIdempotencyUtilsClassName() { + return ClassName.get(getCorePackage(), "IdempotencyUtils"); + } + public ClassName getOkhttp3MediaTypeClassName() { return ClassName.get("okhttp3", "MediaType"); } diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java new file mode 100644 index 000000000000..613bb33a561e --- /dev/null +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java @@ -0,0 +1,44 @@ +/* + * (c) Copyright 2023 Birch Solutions Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fern.java.client.generators; + +import com.fern.java.client.ClientGeneratorContext; +import com.fern.java.generators.AbstractFileGenerator; +import com.fern.java.output.GeneratedResourcesJavaFile; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class IdempotencyUtilsGenerator extends AbstractFileGenerator { + + public IdempotencyUtilsGenerator(ClientGeneratorContext clientGeneratorContext) { + super(clientGeneratorContext.getPoetClassNameFactory().getIdempotencyUtilsClassName(), clientGeneratorContext); + } + + @Override + public GeneratedResourcesJavaFile generateFile() { + try (InputStream is = IdempotencyUtilsGenerator.class.getResourceAsStream("/IdempotencyUtils.java")) { + String contents = new String(is.readAllBytes(), StandardCharsets.UTF_8); + return GeneratedResourcesJavaFile.builder() + .className(className) + .contents(contents) + .build(); + } catch (IOException e) { + throw new RuntimeException("Failed to read IdempotencyUtils.java"); + } + } +} diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/AbstractEndpointWriter.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/AbstractEndpointWriter.java index dcb4bbfa4652..c96a467253b5 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/AbstractEndpointWriter.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/AbstractEndpointWriter.java @@ -20,6 +20,7 @@ import com.fern.ir.model.auth.AuthScope; import com.fern.ir.model.environment.EnvironmentBaseUrlId; import com.fern.ir.model.http.*; +import com.fern.ir.model.ir.IdempotencyKeyGeneration; import com.fern.ir.model.types.*; import com.fern.java.client.ClientGeneratorContext; import com.fern.java.client.GeneratedClientOptions; @@ -636,12 +637,16 @@ public Boolean _visitUnknown(Object unknownType) { httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); methodBody.addStatement( - "$L.headers($T.of(this.$L.$L(($T) null)))", + "$L.headers($T.of($L))", "_requestBuilder", ClassName.get("okhttp3", "Headers"), - clientOptionsField.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - clientGeneratorContext.getPoetClassNameFactory().getRequestOptionsClassName()); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "this.$L.$L(($T) null)", + clientOptionsField.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + clientGeneratorContext + .getPoetClassNameFactory() + .getRequestOptionsClassName()))); methodBody.addStatement( "$T $L = $L.build()", ClassName.get("okhttp3", "Request"), @@ -710,12 +715,16 @@ public Boolean _visitUnknown(Object unknownType) { httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); withMediaTypeBody.addStatement( - "$L.headers($T.of(this.$L.$L(($T) null)))", + "$L.headers($T.of($L))", "_requestBuilder", ClassName.get("okhttp3", "Headers"), - clientOptionsField.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - clientGeneratorContext.getPoetClassNameFactory().getRequestOptionsClassName()); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "this.$L.$L(($T) null)", + clientOptionsField.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + clientGeneratorContext + .getPoetClassNameFactory() + .getRequestOptionsClassName()))); withMediaTypeBody.addStatement( "$T $L = $L.build()", ClassName.get("okhttp3", "Request"), @@ -783,12 +792,14 @@ public Boolean _visitUnknown(Object unknownType) { httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); withRequestOptionsBody.addStatement( - "$L.headers($T.of(this.$L.$L($L)))", + "$L.headers($T.of($L))", "_requestBuilder", ClassName.get("okhttp3", "Headers"), - clientOptionsField.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "this.$L.$L($L)", + clientOptionsField.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); withRequestOptionsBody.addStatement( "$T $L = $L.build()", ClassName.get("okhttp3", "Request"), @@ -848,12 +859,14 @@ public Boolean _visitUnknown(Object unknownType) { httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); withBothBody.addStatement( - "$L.headers($T.of(this.$L.$L($L)))", + "$L.headers($T.of($L))", "_requestBuilder", ClassName.get("okhttp3", "Headers"), - clientOptionsField.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "this.$L.$L($L)", + clientOptionsField.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); withBothBody.addStatement( "$T $L = $L.build()", ClassName.get("okhttp3", "Request"), @@ -899,6 +912,38 @@ public final ParameterSpec requestOptionsParameterSpec() { .build(); } + /** + * Returns the IR-configured idempotency-key auto-generation settings when they apply to this endpoint. Present only + * when the IR's {@code SdkConfig.idempotencyKeyGeneration} is set and this endpoint's HTTP method is one of the + * configured eligible methods. + */ + protected final Optional idempotencyKeyGenerationForEndpoint() { + return clientGeneratorContext + .getIr() + .getSdkConfig() + .getIdempotencyKeyGeneration() + .filter(idempotencyKeyGeneration -> + idempotencyKeyGeneration.getMethods().contains(httpEndpoint.getMethod())); + } + + /** + * Wraps a headers-map expression so that a freshly generated idempotency key is added when the map does not already + * contain one. A caller-provided value always wins. The header name and eligible HTTP methods come from the IR + * ({@code SdkConfig.idempotencyKeyGeneration}). When auto-generation does not apply, the original expression is + * returned unchanged so existing output is byte-identical. + */ + protected final CodeBlock maybeWrapHeadersWithIdempotencyKey(CodeBlock headersExpression) { + Optional idempotencyKeyGeneration = idempotencyKeyGenerationForEndpoint(); + if (idempotencyKeyGeneration.isEmpty()) { + return headersExpression; + } + return CodeBlock.of( + "$T.withGeneratedIdempotencyKey($L, $S)", + clientGeneratorContext.getPoetClassNameFactory().getIdempotencyUtilsClassName(), + headersExpression, + idempotencyKeyGeneration.get().getHeaderName()); + } + protected final MethodSpec getEnvironmentToUrlMethod() { if (generatedEnvironmentsClass.info() instanceof SingleUrlEnvironmentClass) { return ((SingleUrlEnvironmentClass) generatedEnvironmentsClass.info()).getUrlMethod(); diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/NoRequestEndpointWriter.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/NoRequestEndpointWriter.java index 04c0c309726c..a68bb21396df 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/NoRequestEndpointWriter.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/NoRequestEndpointWriter.java @@ -103,14 +103,19 @@ public CodeBlock getInitializeRequestCodeBlock( } if (clientGeneratorContext.isEndpointSecurity()) { - builder.add(".headers($T.of(_headers))\n", Headers.class); + builder.add( + ".headers($T.of($L))\n", + Headers.class, + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of("_headers"))); } else { builder.add( - ".headers($T.of($L.$L($L)))\n", + ".headers($T.of($L))\n", Headers.class, - clientOptionsMember.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "$L.$L($L)", + clientOptionsMember.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); } if (sendContentType) { builder.add(".addHeader($S, $S)\n", AbstractEndpointWriter.CONTENT_TYPE_HEADER, contentType); diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/OnlyRequestEndpointWriter.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/OnlyRequestEndpointWriter.java index 5d92d060a988..99f06451f3b7 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/OnlyRequestEndpointWriter.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/OnlyRequestEndpointWriter.java @@ -163,14 +163,19 @@ public CodeBlock getInitializeRequestCodeBlock( httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); if (clientGeneratorContext.isEndpointSecurity()) { - builder.add(".headers($T.of(_headers))\n", Headers.class); + builder.add( + ".headers($T.of($L))\n", + Headers.class, + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of("_headers"))); } else { builder.add( - ".headers($T.of($L.$L($L)))\n", + ".headers($T.of($L))\n", Headers.class, - clientOptionsMember.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "$L.$L($L)", + clientOptionsMember.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); } if (sendContentType) { sdkRequestBodyType.visit(new SdkRequestBodyType.Visitor() { @@ -292,14 +297,19 @@ public BytesRequest _visitUnknown(Object unknownType) { httpEndpoint.getMethod().toString(), variables.getOkhttpRequestBodyName()); if (clientGeneratorContext.isEndpointSecurity()) { - builder.add(".headers($T.of(_headers))\n", Headers.class); + builder.add( + ".headers($T.of($L))\n", + Headers.class, + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of("_headers"))); } else { builder.add( - ".headers($T.of($L.$L($L)))\n", + ".headers($T.of($L))\n", Headers.class, - clientOptionsMember.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "$L.$L($L)", + clientOptionsMember.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); } if (sendContentType) { builder.add(".addHeader($S, $S)\n", AbstractEndpointWriter.CONTENT_TYPE_HEADER, contentType); diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/WrappedRequestEndpointWriter.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/WrappedRequestEndpointWriter.java index 1e0a0696df78..2bafaec5d709 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/WrappedRequestEndpointWriter.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/endpoint/WrappedRequestEndpointWriter.java @@ -201,14 +201,17 @@ public CodeBlock getInitializeRequestCodeBlock( } Optional maybeAcceptsHeader = AbstractEndpointWriter.maybeAcceptsHeader(httpEndpoint); if (clientGeneratorContext.isEndpointSecurity()) { - requestBodyCodeBlock.add(".headers($T.of(_headers))", Headers.class); + requestBodyCodeBlock.add( + ".headers($T.of($L))", Headers.class, maybeWrapHeadersWithIdempotencyKey(CodeBlock.of("_headers"))); } else { requestBodyCodeBlock.add( - ".headers($T.of($L.$L($L)))", + ".headers($T.of($L))", Headers.class, - clientOptionsMember.name, - ClientOptionsGenerator.HEADERS_METHOD_NAME, - AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME); + maybeWrapHeadersWithIdempotencyKey(CodeBlock.of( + "$L.$L($L)", + clientOptionsMember.name, + ClientOptionsGenerator.HEADERS_METHOD_NAME, + AbstractEndpointWriterVariableNameContext.REQUEST_OPTIONS_PARAMETER_NAME))); } if (sendContentType && !isFileUpload) { requestBodyCodeBlock.add("\n.addHeader($S, $S)", AbstractEndpointWriter.CONTENT_TYPE_HEADER, contentType); diff --git a/generators/java/sdk/src/main/resources/IdempotencyUtils.java b/generators/java/sdk/src/main/resources/IdempotencyUtils.java new file mode 100644 index 000000000000..11dadbf57724 --- /dev/null +++ b/generators/java/sdk/src/main/resources/IdempotencyUtils.java @@ -0,0 +1,25 @@ +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Utilities for auto-generating idempotency keys on mutating requests. + * + *

Only emitted when idempotency-key auto-generation is enabled in the IR + * ({@code SdkConfig.idempotencyKeyGeneration}). + */ +public final class IdempotencyUtils { + + private IdempotencyUtils() {} + + /** + * Returns a copy of {@code headers} with a freshly generated UUIDv4 value added under {@code headerName} when a + * value is not already present. A caller-provided value (whether from a declared idempotency header or the generic + * request-options header bag) always wins. + */ + public static Map withGeneratedIdempotencyKey(Map headers, String headerName) { + Map result = new HashMap<>(headers); + result.putIfAbsent(headerName, UUID.randomUUID().toString()); + return result; + } +} diff --git a/packages/commons/api-workspace-commons/src/checkVersionExists.ts b/packages/commons/api-workspace-commons/src/checkVersionExists.ts index 08ebd081beb0..874ff6046aab 100644 --- a/packages/commons/api-workspace-commons/src/checkVersionExists.ts +++ b/packages/commons/api-workspace-commons/src/checkVersionExists.ts @@ -100,6 +100,28 @@ function parseIdempotencyKeyMethods(value: unknown): HttpMethod[] { return methods.length > 0 ? methods : DEFAULT_IDEMPOTENCY_KEY_METHODS; } +/** + * Resolves the object to read generator config keys from. + * + * Prefers the raw (unvalidated) generators.yml config, which is always present in the + * production generation path and preserves CLI-only keys verbatim. Falls back to the resolved + * `config` when `raw` is absent — this happens for synthetic invocations (e.g. the seed test + * harness) that populate `config` directly without a `raw` block. + */ +function getGeneratorConfigObject( + generatorInvocation: generatorsYml.GeneratorInvocation +): Record | undefined { + const raw = generatorInvocation.raw?.config; + if (typeof raw === "object" && raw !== null) { + return raw as Record; + } + const resolved = generatorInvocation.config; + if (typeof resolved === "object" && resolved !== null) { + return resolved as Record; + } + return undefined; +} + /** * Resolves the idempotency-key auto-generation config from the raw generator configuration. * @@ -123,12 +145,11 @@ function parseIdempotencyKeyMethods(value: unknown): HttpMethod[] { export function getIdempotencyKeyGenerationFromGeneratorConfig( generatorInvocation: generatorsYml.GeneratorInvocation ): IdempotencyKeyGeneration | undefined { - if (typeof generatorInvocation.raw?.config !== "object" || generatorInvocation.raw?.config === null) { + const config = getGeneratorConfigObject(generatorInvocation); + if (config == null) { return undefined; } - const value = (generatorInvocation.raw.config as { "auto-generate-idempotency-key"?: unknown })[ - "auto-generate-idempotency-key" - ]; + const value = config["auto-generate-idempotency-key"]; if (value === true) { return { headerName: DEFAULT_IDEMPOTENCY_KEY_HEADER, methods: DEFAULT_IDEMPOTENCY_KEY_METHODS }; } diff --git a/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.fern/metadata.json b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.fern/metadata.json new file mode 100644 index 000000000000..e788dc71dba2 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.fern/metadata.json @@ -0,0 +1,13 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-java-sdk", + "generatorVersion": "local", + "generatorConfig": { + "auto-generate-idempotency-key": true + }, + "originGitCommit": "DUMMY", + "invokedBy": "ci", + "requestedVersion": "0.0.1", + "ciProvider": "github", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/java-sdk/idempotency-headers/.github/workflows/ci.yml b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.github/workflows/ci.yml similarity index 100% rename from seed/java-sdk/idempotency-headers/.github/workflows/ci.yml rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.github/workflows/ci.yml diff --git a/seed/java-sdk/idempotency-headers/.gitignore b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.gitignore similarity index 100% rename from seed/java-sdk/idempotency-headers/.gitignore rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/.gitignore diff --git a/seed/java-sdk/idempotency-headers/CONTRIBUTING.md b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/CONTRIBUTING.md similarity index 100% rename from seed/java-sdk/idempotency-headers/CONTRIBUTING.md rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/CONTRIBUTING.md diff --git a/seed/java-sdk/idempotency-headers/README.md b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/README.md similarity index 100% rename from seed/java-sdk/idempotency-headers/README.md rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/README.md diff --git a/seed/java-sdk/idempotency-headers/build.gradle b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/build.gradle similarity index 100% rename from seed/java-sdk/idempotency-headers/build.gradle rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/build.gradle diff --git a/seed/java-sdk/idempotency-headers/reference.md b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/reference.md similarity index 100% rename from seed/java-sdk/idempotency-headers/reference.md rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/reference.md diff --git a/seed/java-sdk/idempotency-headers/sample-app/build.gradle b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/sample-app/build.gradle similarity index 100% rename from seed/java-sdk/idempotency-headers/sample-app/build.gradle rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/sample-app/build.gradle diff --git a/seed/java-sdk/idempotency-headers/sample-app/src/main/java/sample/App.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java similarity index 100% rename from seed/java-sdk/idempotency-headers/sample-app/src/main/java/sample/App.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java diff --git a/seed/java-sdk/idempotency-headers/settings.gradle b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/settings.gradle similarity index 100% rename from seed/java-sdk/idempotency-headers/settings.gradle rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/settings.gradle diff --git a/seed/java-sdk/idempotency-headers/snippet.json b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/snippet.json similarity index 100% rename from seed/java-sdk/idempotency-headers/snippet.json rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/snippet.json diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Environment.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Environment.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Environment.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Environment.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java diff --git a/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotencyUtils.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotencyUtils.java new file mode 100644 index 000000000000..ab18cad0d0a8 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotencyUtils.java @@ -0,0 +1,30 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Utilities for auto-generating idempotency keys on mutating requests. + * + *

Only emitted when idempotency-key auto-generation is enabled in the IR + * ({@code SdkConfig.idempotencyKeyGeneration}). + */ +public final class IdempotencyUtils { + + private IdempotencyUtils() {} + + /** + * Returns a copy of {@code headers} with a freshly generated UUIDv4 value added under {@code headerName} when a + * value is not already present. A caller-provided value (whether from a declared idempotency header or the generic + * request-options header bag) always wins. + */ + public static Map withGeneratedIdempotencyKey(Map headers, String headerName) { + Map result = new HashMap<>(headers); + result.putIfAbsent(headerName, UUID.randomUUID().toString()); + return result; + } +} diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Logger.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Logger.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Logger.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Logger.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Stream.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Stream.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Stream.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Stream.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java diff --git a/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java new file mode 100644 index 000000000000..d47a29776e52 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java @@ -0,0 +1,178 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.IdempotencyUtils; +import com.seed.idempotencyHeaders.core.IdempotentRequestOptions; +import com.seed.idempotencyHeaders.core.MediaTypes; +import com.seed.idempotencyHeaders.core.ObjectMappers; +import com.seed.idempotencyHeaders.core.RequestOptions; +import com.seed.idempotencyHeaders.core.RetryInterceptor; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersApiException; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersException; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersHttpResponse; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawPaymentClient { + protected final ClientOptions clientOptions; + + public AsyncRawPaymentClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public CompletableFuture> create(CreatePaymentRequest request) { + return create(request, null); + } + + public CompletableFuture> create( + CreatePaymentRequest request, IdempotentRequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("payment"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedIdempotencyHeadersException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + clientOptions.headers(requestOptions), "Idempotency-Key"))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedIdempotencyHeadersHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UUID.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedIdempotencyHeadersApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedIdempotencyHeadersException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally( + new SeedIdempotencyHeadersException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedIdempotencyHeadersException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> delete(String paymentId) { + return delete(paymentId, null); + } + + public CompletableFuture> delete( + String paymentId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("payment") + .addPathSegment(paymentId); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("DELETE", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new SeedIdempotencyHeadersHttpResponse<>(null, response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedIdempotencyHeadersApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedIdempotencyHeadersException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally( + new SeedIdempotencyHeadersException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedIdempotencyHeadersException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java diff --git a/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java new file mode 100644 index 000000000000..75fceb1b37ee --- /dev/null +++ b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java @@ -0,0 +1,141 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.IdempotencyUtils; +import com.seed.idempotencyHeaders.core.IdempotentRequestOptions; +import com.seed.idempotencyHeaders.core.MediaTypes; +import com.seed.idempotencyHeaders.core.ObjectMappers; +import com.seed.idempotencyHeaders.core.RequestOptions; +import com.seed.idempotencyHeaders.core.RetryInterceptor; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersApiException; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersException; +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersHttpResponse; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import java.io.IOException; +import java.util.UUID; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawPaymentClient { + protected final ClientOptions clientOptions; + + public RawPaymentClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public SeedIdempotencyHeadersHttpResponse create(CreatePaymentRequest request) { + return create(request, null); + } + + public SeedIdempotencyHeadersHttpResponse create( + CreatePaymentRequest request, IdempotentRequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("payment"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new SeedIdempotencyHeadersException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + clientOptions.headers(requestOptions), "Idempotency-Key"))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedIdempotencyHeadersHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UUID.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedIdempotencyHeadersApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedIdempotencyHeadersException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedIdempotencyHeadersException("Network error executing HTTP request", e); + } + } + + public SeedIdempotencyHeadersHttpResponse delete(String paymentId) { + return delete(paymentId, null); + } + + public SeedIdempotencyHeadersHttpResponse delete(String paymentId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("payment") + .addPathSegment(paymentId); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("DELETE", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new SeedIdempotencyHeadersHttpResponse<>(null, response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedIdempotencyHeadersApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedIdempotencyHeadersException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedIdempotencyHeadersException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/snippets/Example0.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/snippets/Example0.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/snippets/Example0.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/snippets/Example0.java diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/snippets/Example1.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/snippets/Example1.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/snippets/Example1.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/main/java/com/snippets/Example1.java diff --git a/seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/StreamTest.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/StreamTest.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/StreamTest.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/StreamTest.java diff --git a/seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/TestClient.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/TestClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/TestClient.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/TestClient.java diff --git a/seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java b/seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java rename to seed/java-sdk/idempotency-headers/auto-generate-idempotency-key/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java diff --git a/seed/java-sdk/idempotency-headers/.fern/metadata.json b/seed/java-sdk/idempotency-headers/no-custom-config/.fern/metadata.json similarity index 100% rename from seed/java-sdk/idempotency-headers/.fern/metadata.json rename to seed/java-sdk/idempotency-headers/no-custom-config/.fern/metadata.json diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/.github/workflows/ci.yml b/seed/java-sdk/idempotency-headers/no-custom-config/.github/workflows/ci.yml new file mode 100644 index 000000000000..09c8c666ad73 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Compile + run: ./gradlew compileJava + + test: + needs: [ compile ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Test + run: ./gradlew test + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Publish to maven + run: | + ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLISH_REGISTRY_URL: "" diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/.gitignore b/seed/java-sdk/idempotency-headers/no-custom-config/.gitignore new file mode 100644 index 000000000000..d4199abc2cd4 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/.gitignore @@ -0,0 +1,24 @@ +*.class +.project +.gradle +? +.classpath +.checkstyle +.settings +.node +build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Eclipse/IntelliJ APT +generated_src/ +generated_testSrc/ +generated/ + +bin +build \ No newline at end of file diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/CONTRIBUTING.md b/seed/java-sdk/idempotency-headers/no-custom-config/CONTRIBUTING.md new file mode 100644 index 000000000000..24cf9fa0c645 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Java 11+ +- Gradle + +### Installation + +Install the project dependencies: + +```bash +./gradlew build +``` + +### Building + +Build the project: + +```bash +./gradlew build +``` + +### Testing + +Run the test suite: + +```bash +./gradlew test +``` + +### Formatting + +Check and fix code style: + +```bash +./gradlew spotlessApply +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Java files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Java SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/java-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `./gradlew test` +4. Run formatting: `./gradlew spotlessApply` +5. Build the project: `./gradlew build` +6. Commit your changes with a clear commit message +7. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting. Run `./gradlew spotlessApply` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/README.md b/seed/java-sdk/idempotency-headers/no-custom-config/README.md new file mode 100644 index 000000000000..30533a942361 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/README.md @@ -0,0 +1,229 @@ +# Seed Java 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%2FJava) +[![Maven Central](https://img.shields.io/maven-central/v/com.fern/idempotency-headers)](https://central.sonatype.com/artifact/com.fern/idempotency-headers) + +The Seed Java library provides convenient access to the Seed APIs from Java. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Base Url](#base-url) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Custom Client](#custom-client) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Headers](#custom-headers) + - [Access Raw Response Data](#access-raw-response-data) +- [Contributing](#contributing) + +## Installation + +### Gradle + +Add the dependency in your `build.gradle` file: + +```groovy +dependencies { + implementation 'com.fern:idempotency-headers:0.0.1' +} +``` + +### Maven + +Add the dependency in your `pom.xml` file: + +```xml + + com.fern + idempotency-headers + 0.0.1 + +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```java +package com.example.usage; + +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import com.seed.idempotencyHeaders.resources.payment.types.Currency; + +public class Example { + public static void main(String[] args) { + SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .token("") + .build(); + + client.payment().create( + CreatePaymentRequest + .builder() + .amount(1) + .currency(Currency.USD) + .build() + ); + } +} +``` + +## Base Url + +You can set a custom base URL when constructing the client. + +```java +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; + +SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .url("https://example.com") + .build(); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), an API exception will be thrown. + +```java +import com.seed.idempotencyHeaders.core.SeedIdempotencyHeadersApiException; + +try{ + client.payment().create(...); +} catch (SeedIdempotencyHeadersApiException e){ + // Do something with the API exception... +} +``` + +## Advanced + +### Custom Client + +This SDK is built to work with any instance of `OkHttpClient`. By default, if no client is provided, the SDK will construct one. +However, you can pass your own client like so: + +```java +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; +import okhttp3.OkHttpClient; + +OkHttpClient customClient = ...; + +SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .httpClient(customClient) + .build(); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). Before defaulting to exponential backoff, the SDK will first attempt to respect +the `Retry-After` header (as either in seconds or as an HTTP date), and then the `X-RateLimit-Reset` header +(as a Unix timestamp in epoch seconds); failing both of those, it will fall back to exponential backoff. + +Which status codes are retried depends on the `retry-status-codes` generator configuration: + +**`legacy`** (current default): retries on +- [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#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [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) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `maxRetries` client option to configure this behavior. + +```java +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; + +SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .maxRetries(1) + .build(); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. +```java +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; +import com.seed.idempotencyHeaders.core.RequestOptions; + +// Client level +SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .timeout(60) + .build(); + +// Request level +client.payment().create( + ..., + RequestOptions + .builder() + .timeout(60) + .build() +); +``` + +### Custom Headers + +The SDK allows you to add custom headers to requests. You can configure headers at the client level or at the request level. + +```java +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; +import com.seed.idempotencyHeaders.core.RequestOptions; + +// Client level +SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient + .builder() + .addHeader("X-Custom-Header", "custom-value") + .addHeader("X-Request-Id", "abc-123") + .build(); +; + +// Request level +client.payment().create( + ..., + RequestOptions + .builder() + .addHeader("X-Request-Header", "request-value") + .build() +); +``` + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `withRawResponse()` method. +The `withRawResponse()` method returns a raw client that wraps all responses with `body()` and `headers()` methods. +(A normal client's `response` is identical to a raw client's `response.body()`.) + +```java +SeedIdempotencyHeadersHttpResponse response = client.payment().withRawResponse().create(...); + +System.out.println(response.body()); +System.out.println(response.headers().get("X-My-Header")); +``` + +## 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/java-sdk/idempotency-headers/no-custom-config/build.gradle b/seed/java-sdk/idempotency-headers/no-custom-config/build.gradle new file mode 100644 index 000000000000..055341404442 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/build.gradle @@ -0,0 +1,102 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.11.0' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + api 'com.squareup.okhttp3:okhttp:5.2.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.18.6' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.6' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.6' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +tasks.withType(Javadoc) { + failOnError false + options.addStringOption('Xdoclint:none', '-quiet') +} + +spotless { + java { + palantirJavaFormat() + } +} + + +java { + withSourcesJar() + withJavadocJar() +} + + +group = 'com.fern' + +version = '0.0.1' + +jar { + dependsOn(":generatePomFileForMavenPublication") + archiveBaseName = "idempotency-headers" +} + +sourcesJar { + archiveBaseName = "idempotency-headers" +} + +javadocJar { + archiveBaseName = "idempotency-headers" +} + +test { + useJUnitPlatform() + testLogging { + showStandardStreams = true + } +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'com.fern' + artifactId = 'idempotency-headers' + version = '0.0.1' + from components.java + pom { + licenses { + license { + name = 'The MIT License (MIT)' + url = 'https://mit-license.org/' + } + } + scm { + connection = 'scm:git:git://github.com/idempotency-headers/fern.git' + developerConnection = 'scm:git:git://github.com/idempotency-headers/fern.git' + url = 'https://github.com/idempotency-headers/fern' + } + } + } + } + repositories { + maven { + url "$System.env.MAVEN_PUBLISH_REGISTRY_URL" + credentials { + username "$System.env.MAVEN_USERNAME" + password "$System.env.MAVEN_PASSWORD" + } + } + } +} + diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/reference.md b/seed/java-sdk/idempotency-headers/no-custom-config/reference.md new file mode 100644 index 000000000000..f5a9c4a7ecff --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/reference.md @@ -0,0 +1,96 @@ +# Reference +## Payment +

client.payment.create(request) -> UUID +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.payment().create( + CreatePaymentRequest + .builder() + .amount(1) + .currency(Currency.USD) + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**amount:** `Integer` + +
+
+ +
+
+ +**currency:** `Currency` + +
+
+
+
+ + +
+
+
+ +
client.payment.delete(paymentId) +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.payment().delete("paymentId"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**paymentId:** `String` + +
+
+
+
+ + +
+
+
+ diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/build.gradle b/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/build.gradle new file mode 100644 index 000000000000..4ee8f227b7af --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + implementation rootProject +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/src/main/java/sample/App.java b/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/src/main/java/sample/App.java new file mode 100644 index 000000000000..a75632a06948 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/sample-app/src/main/java/sample/App.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package sample; + +import java.lang.String; + +public final class App { + public static void main(String[] args) { + // import com.seed.idempotencyHeaders.AsyncSeedIdempotencyHeadersClient + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/settings.gradle b/seed/java-sdk/idempotency-headers/no-custom-config/settings.gradle new file mode 100644 index 000000000000..c8a19ab15e2b --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'idempotency-headers' + +include 'sample-app' \ No newline at end of file diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/snippet.json b/seed/java-sdk/idempotency-headers/no-custom-config/snippet.json new file mode 100644 index 000000000000..90760c430385 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/snippet.json @@ -0,0 +1,31 @@ +{ + "endpoints": [ + { + "example_identifier": "c4af9402", + "id": { + "method": "POST", + "path": "/payment", + "identifier_override": "endpoint_payment.create" + }, + "snippet": { + "type": "java", + "sync_client": "package com.example.usage;\n\nimport com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient;\nimport com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest;\nimport com.seed.idempotencyHeaders.resources.payment.types.Currency;\n\npublic class Example {\n public static void main(String[] args) {\n SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient\n .builder()\n .token(\"\")\n .build();\n\n client.payment().create(\n CreatePaymentRequest\n .builder()\n .amount(1)\n .currency(Currency.USD)\n .build()\n );\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient;\nimport com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest;\nimport com.seed.idempotencyHeaders.resources.payment.types.Currency;\n\npublic class Example {\n public static void main(String[] args) {\n SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient\n .builder()\n .token(\"\")\n .build();\n\n client.payment().create(\n CreatePaymentRequest\n .builder()\n .amount(1)\n .currency(Currency.USD)\n .build()\n );\n }\n}\n" + } + }, + { + "example_identifier": "4bc590fe", + "id": { + "method": "DELETE", + "path": "/payment/{paymentId}", + "identifier_override": "endpoint_payment.delete" + }, + "snippet": { + "type": "java", + "sync_client": "package com.example.usage;\n\nimport com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient\n .builder()\n .token(\"\")\n .build();\n\n client.payment().delete(\"paymentId\");\n }\n}\n", + "async_client": "package com.example.usage;\n\nimport com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient;\n\npublic class Example {\n public static void main(String[] args) {\n SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient\n .builder()\n .token(\"\")\n .build();\n\n client.payment().delete(\"paymentId\");\n }\n}\n" + } + } + ], + "types": {} +} \ No newline at end of file diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java new file mode 100644 index 000000000000..11481944dd1d --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.Suppliers; +import com.seed.idempotencyHeaders.resources.payment.AsyncPaymentClient; +import java.util.function.Supplier; + +public class AsyncSeedIdempotencyHeadersClient { + protected final ClientOptions clientOptions; + + protected final Supplier paymentClient; + + public AsyncSeedIdempotencyHeadersClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.paymentClient = Suppliers.memoize(() -> new AsyncPaymentClient(clientOptions)); + } + + public AsyncPaymentClient payment() { + return this.paymentClient.get(); + } + + public static AsyncSeedIdempotencyHeadersClientBuilder builder() { + return new AsyncSeedIdempotencyHeadersClientBuilder(); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java new file mode 100644 index 000000000000..1a82e220a4e1 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/AsyncSeedIdempotencyHeadersClientBuilder.java @@ -0,0 +1,263 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.Environment; +import com.seed.idempotencyHeaders.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class AsyncSeedIdempotencyHeadersClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String token = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + /** + * Sets token + */ + public AsyncSeedIdempotencyHeadersClientBuilder token(String token) { + this.token = token; + return this; + } + + public AsyncSeedIdempotencyHeadersClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public AsyncSeedIdempotencyHeadersClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public AsyncSeedIdempotencyHeadersClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public AsyncSeedIdempotencyHeadersClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public AsyncSeedIdempotencyHeadersClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Sets the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public AsyncSeedIdempotencyHeadersClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public AsyncSeedIdempotencyHeadersClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public AsyncSeedIdempotencyHeadersClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public AsyncSeedIdempotencyHeadersClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.token != null) { + builder.addHeader("Authorization", "Bearer " + this.token); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + if (this.initialRetryDelayMillis.isPresent()) { + builder.initialRetryDelayMillis(this.initialRetryDelayMillis.get()); + } + if (this.maxRetryDelayMillis.isPresent()) { + builder.maxRetryDelayMillis(this.maxRetryDelayMillis.get()); + } + if (this.retryJitterFactor.isPresent()) { + builder.retryJitterFactor(this.retryJitterFactor.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public AsyncSeedIdempotencyHeadersClient build() { + if (token == null) { + throw new RuntimeException("Please provide token"); + } + validateConfiguration(); + return new AsyncSeedIdempotencyHeadersClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java new file mode 100644 index 000000000000..12ba4d2949d2 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.Suppliers; +import com.seed.idempotencyHeaders.resources.payment.PaymentClient; +import java.util.function.Supplier; + +public class SeedIdempotencyHeadersClient { + protected final ClientOptions clientOptions; + + protected final Supplier paymentClient; + + public SeedIdempotencyHeadersClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.paymentClient = Suppliers.memoize(() -> new PaymentClient(clientOptions)); + } + + public PaymentClient payment() { + return this.paymentClient.get(); + } + + public static SeedIdempotencyHeadersClientBuilder builder() { + return new SeedIdempotencyHeadersClientBuilder(); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java new file mode 100644 index 000000000000..cf943601438c --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/SeedIdempotencyHeadersClientBuilder.java @@ -0,0 +1,263 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.Environment; +import com.seed.idempotencyHeaders.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class SeedIdempotencyHeadersClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String token = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + /** + * Sets token + */ + public SeedIdempotencyHeadersClientBuilder token(String token) { + this.token = token; + return this; + } + + public SeedIdempotencyHeadersClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public SeedIdempotencyHeadersClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public SeedIdempotencyHeadersClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public SeedIdempotencyHeadersClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public SeedIdempotencyHeadersClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Sets the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public SeedIdempotencyHeadersClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public SeedIdempotencyHeadersClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public SeedIdempotencyHeadersClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public SeedIdempotencyHeadersClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.token != null) { + builder.addHeader("Authorization", "Bearer " + this.token); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + if (this.initialRetryDelayMillis.isPresent()) { + builder.initialRetryDelayMillis(this.initialRetryDelayMillis.get()); + } + if (this.maxRetryDelayMillis.isPresent()) { + builder.maxRetryDelayMillis(this.maxRetryDelayMillis.get()); + } + if (this.retryJitterFactor.isPresent()) { + builder.retryJitterFactor(this.retryJitterFactor.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public SeedIdempotencyHeadersClient build() { + if (token == null) { + throw new RuntimeException("Please provide token"); + } + validateConfiguration(); + return new SeedIdempotencyHeadersClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java new file mode 100644 index 000000000000..1ea88b766d27 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ClientOptions.java @@ -0,0 +1,310 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import okhttp3.OkHttpClient; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private final int timeout; + + private final int maxRetries; + + private final Optional initialRetryDelayMillis; + + private final Optional maxRetryDelayMillis; + + private final Optional retryJitterFactor; + + private final Optional logging; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient, + int timeout, + int maxRetries, + Optional initialRetryDelayMillis, + Optional maxRetryDelayMillis, + Optional retryJitterFactor, + Optional logging) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("User-Agent", "com.fern.idempotency-headers/0.0.1"); + put("X-Fern-Language", "JAVA"); + put("X-Fern-SDK-Name", "com.seed.fern:idempotency-headers-sdk"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + this.timeout = timeout; + this.maxRetries = maxRetries; + this.initialRetryDelayMillis = initialRetryDelayMillis; + this.maxRetryDelayMillis = maxRetryDelayMillis; + this.retryJitterFactor = retryJitterFactor; + this.logging = logging; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public Map headers(IdempotentRequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public OkHttpClient httpClientWithTimeout(IdempotentRequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int timeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.timeout; + } + return requestOptions.getTimeout().orElse(this.timeout); + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int maxRetries() { + return this.maxRetries; + } + + public Optional initialRetryDelayMillis() { + return this.initialRetryDelayMillis; + } + + public Optional maxRetryDelayMillis() { + return this.maxRetryDelayMillis; + } + + public Optional retryJitterFactor() { + return this.retryJitterFactor; + } + + public Optional logging() { + return this.logging; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private int maxRetries = 2; + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private Optional timeout = Optional.empty(); + + private OkHttpClient httpClient = null; + + private Optional logging = Optional.empty(); + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(Optional timeout) { + this.timeout = timeout; + return this; + } + + /** + * Override the maximum number of retries. Defaults to 2 retries. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Override the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public Builder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Override the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public Builder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Override the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public Builder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public Builder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + public ClientOptions build() { + OkHttpClient.Builder httpClientBuilder = + this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); + + if (this.httpClient != null) { + timeout.ifPresent(timeout -> httpClientBuilder + .callTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS)); + } else { + httpClientBuilder + .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .addInterceptor(new RetryInterceptor( + this.maxRetries, + this.initialRetryDelayMillis, + this.maxRetryDelayMillis, + this.retryJitterFactor)); + } + + Logger logger = Logger.from(this.logging); + httpClientBuilder.addInterceptor(new LoggingInterceptor(logger)); + httpClientBuilder.addInterceptor(new ResponseDecompressionInterceptor()); + + this.httpClient = httpClientBuilder.build(); + this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); + + return new ClientOptions( + environment, + headers, + headerSuppliers, + httpClient, + this.timeout.get(), + this.maxRetries, + this.initialRetryDelayMillis, + this.maxRetryDelayMillis, + this.retryJitterFactor, + this.logging); + } + + /** + * Create a new Builder initialized with values from an existing ClientOptions + */ + public static Builder from(ClientOptions clientOptions) { + Builder builder = new Builder(); + builder.environment = clientOptions.environment(); + builder.timeout = Optional.of(clientOptions.timeout(null)); + builder.httpClient = clientOptions.httpClient(); + builder.headers.putAll(clientOptions.headers); + builder.headerSuppliers.putAll(clientOptions.headerSuppliers); + builder.maxRetries = clientOptions.maxRetries(); + builder.initialRetryDelayMillis = clientOptions.initialRetryDelayMillis(); + builder.maxRetryDelayMillis = clientOptions.maxRetryDelayMillis(); + builder.retryJitterFactor = clientOptions.retryJitterFactor(); + builder.logging = clientOptions.logging(); + return builder; + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java new file mode 100644 index 000000000000..e12e61c7a0f6 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ConsoleLogger.java @@ -0,0 +1,51 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.logging.Level; + +/** + * Default logger implementation that writes to the console using {@link java.util.logging.Logger}. + * + *

Uses the "fern" logger name with a simple format of "LEVEL - message". + */ +public final class ConsoleLogger implements ILogger { + + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("fern"); + + static { + if (logger.getHandlers().length == 0) { + java.util.logging.ConsoleHandler handler = new java.util.logging.ConsoleHandler(); + handler.setFormatter(new java.util.logging.SimpleFormatter() { + @Override + public String format(java.util.logging.LogRecord record) { + return record.getLevel() + " - " + record.getMessage() + System.lineSeparator(); + } + }); + logger.addHandler(handler); + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + } + } + + @Override + public void debug(String message) { + logger.log(Level.FINE, message); + } + + @Override + public void info(String message) { + logger.log(Level.INFO, message); + } + + @Override + public void warn(String message) { + logger.log(Level.WARNING, message); + } + + @Override + public void error(String message) { + logger.log(Level.SEVERE, message); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java new file mode 100644 index 000000000000..cd57aa736891 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DateTimeDeserializer.java @@ -0,0 +1,70 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting date-time strings into {@link OffsetDateTime} objects. + * Supports ISO 8601 format, space-separated variants, and RFC 1123 (RFC 2822) format. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + String value = parser.getValueAsString(); + TemporalAccessor temporal; + try { + temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest(value, OffsetDateTime::from, LocalDateTime::from); + } catch (DateTimeParseException e) { + try { + // Fall back to space-separated format (e.g. "2025-02-15 10:30:00+00:00"). + temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + value.replace(' ', 'T'), OffsetDateTime::from, LocalDateTime::from); + } catch (DateTimeParseException e2) { + // Fall back to RFC 1123 format (e.g. "Thu, 07 May 2026 14:23:38 +0000"). + temporal = DateTimeFormatter.RFC_1123_DATE_TIME.parseBest( + value, OffsetDateTime::from, LocalDateTime::from); + } + } + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java new file mode 100644 index 000000000000..38d286c4fb1b --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/DoubleSerializer.java @@ -0,0 +1,43 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; + +/** + * Custom serializer that writes integer-valued doubles without a decimal point. + * For example, {@code 24000.0} is serialized as {@code 24000} instead of {@code 24000.0}. + * Non-integer values like {@code 3.14} are serialized normally. + */ +class DoubleSerializer extends JsonSerializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule() + .addSerializer(Double.class, new DoubleSerializer()) + .addSerializer(double.class, new DoubleSerializer()); + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null && value == Math.floor(value) && !Double.isInfinite(value) && !Double.isNaN(value)) { + gen.writeNumber(value.longValue()); + } else { + gen.writeNumber(value); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Environment.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Environment.java new file mode 100644 index 000000000000..f8274c277396 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java new file mode 100644 index 000000000000..4534c850c373 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/FileStream.java @@ -0,0 +1,60 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java new file mode 100644 index 000000000000..aefa242ef53e --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ILogger.java @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +/** + * Interface for custom logger implementations. + * + *

Implement this interface to provide a custom logging backend for the SDK. + * The SDK will call the appropriate method based on the log level. + * + *

Example: + *

{@code
+ * public class MyCustomLogger implements ILogger {
+ *     public void debug(String message) {
+ *         System.out.println("[DBG] " + message);
+ *     }
+ *     public void info(String message) {
+ *         System.out.println("[INF] " + message);
+ *     }
+ *     public void warn(String message) {
+ *         System.out.println("[WRN] " + message);
+ *     }
+ *     public void error(String message) {
+ *         System.out.println("[ERR] " + message);
+ *     }
+ * }
+ * }
+ */ +public interface ILogger { + void debug(String message); + + void info(String message); + + void warn(String message); + + void error(String message); +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java new file mode 100644 index 000000000000..b1e947b77755 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/IdempotentRequestOptions.java @@ -0,0 +1,184 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class IdempotentRequestOptions { + private final String token; + + private final String idempotencyKey; + + private final String idempotencyExpiration; + + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Optional maxRetries; + + private final Map headers; + + private final Map> headerSuppliers; + + private final Map queryParameters; + + private final Map> queryParameterSuppliers; + + private IdempotentRequestOptions( + String token, + String idempotencyKey, + String idempotencyExpiration, + Optional timeout, + TimeUnit timeoutTimeUnit, + Optional maxRetries, + Map headers, + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { + this.token = token; + this.idempotencyKey = idempotencyKey; + this.idempotencyExpiration = idempotencyExpiration; + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.maxRetries = maxRetries; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Optional getMaxRetries() { + return maxRetries; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + if (this.token != null) { + headers.put("Authorization", "Bearer " + this.token); + } + if (this.idempotencyKey != null) { + headers.put("Idempotency-Key", this.idempotencyKey); + } + if (this.idempotencyExpiration != null) { + headers.put("Idempotency-Expiration", this.idempotencyExpiration); + } + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public Map getQueryParameters() { + Map queryParameters = new HashMap<>(this.queryParameters); + this.queryParameterSuppliers.forEach((key, supplier) -> { + queryParameters.put(key, supplier.get()); + }); + return queryParameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String token = null; + + private String idempotencyKey = null; + + private String idempotencyExpiration = null; + + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private Optional maxRetries = Optional.empty(); + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private final Map queryParameters = new HashMap<>(); + + private final Map> queryParameterSuppliers = new HashMap<>(); + + public Builder token(String token) { + this.token = token; + return this; + } + + public Builder idempotencyKey(String idempotencyKey) { + this.idempotencyKey = idempotencyKey; + return this; + } + + public Builder idempotencyExpiration(String idempotencyExpiration) { + this.idempotencyExpiration = idempotencyExpiration; + return this; + } + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, String value) { + this.queryParameters.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, Supplier value) { + this.queryParameterSuppliers.put(key, value); + return this; + } + + public IdempotentRequestOptions build() { + return new IdempotentRequestOptions( + token, + idempotencyKey, + idempotencyExpiration, + timeout, + timeoutTimeUnit, + maxRetries, + headers, + headerSuppliers, + queryParameters, + queryParameterSuppliers); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java new file mode 100644 index 000000000000..2748763d58a3 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/InputStreamRequestBody.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (Source source = Okio.source(inputStream)) { + sink.writeAll(source); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java new file mode 100644 index 000000000000..49848a9ae97d --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogConfig.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +/** + * Configuration for SDK logging. + * + *

Use the builder to configure logging behavior: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Or with a custom logger: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .logger(new MyCustomLogger())
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Defaults: + *

    + *
  • {@code level} — {@link LogLevel#INFO}
  • + *
  • {@code logger} — {@link ConsoleLogger} (writes to stderr via java.util.logging)
  • + *
  • {@code silent} — {@code true} (no output unless explicitly enabled)
  • + *
+ */ +public final class LogConfig { + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + private LogConfig(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + public LogLevel level() { + return level; + } + + public ILogger logger() { + return logger; + } + + public boolean silent() { + return silent; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private LogLevel level = LogLevel.INFO; + private ILogger logger = new ConsoleLogger(); + private boolean silent = true; + + private Builder() {} + + /** + * Set the minimum log level. Only messages at this level or above will be logged. + * Defaults to {@link LogLevel#INFO}. + */ + public Builder level(LogLevel level) { + this.level = level; + return this; + } + + /** + * Set a custom logger implementation. Defaults to {@link ConsoleLogger}. + */ + public Builder logger(ILogger logger) { + this.logger = logger; + return this; + } + + /** + * Set whether logging is silent (disabled). Defaults to {@code true}. + * Set to {@code false} to enable log output. + */ + public Builder silent(boolean silent) { + this.silent = silent; + return this; + } + + public LogConfig build() { + return new LogConfig(level, logger, silent); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java new file mode 100644 index 000000000000..280b8e844a05 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LogLevel.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +/** + * Log levels for SDK logging configuration. + * Silent by default — no log output unless explicitly configured. + */ +public enum LogLevel { + DEBUG(1), + INFO(2), + WARN(3), + ERROR(4); + + private final int value; + + LogLevel(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + /** + * Parse a log level from a string (case-insensitive). + * + * @param level the level string (debug, info, warn, error) + * @return the corresponding LogLevel + * @throws IllegalArgumentException if the string does not match any level + */ + public static LogLevel fromString(String level) { + return LogLevel.valueOf(level.toUpperCase()); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Logger.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Logger.java new file mode 100644 index 000000000000..6ec321f65fb2 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Logger.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +/** + * SDK logger that filters messages based on level and silent mode. + * + *

Silent by default — no log output unless explicitly configured. + * Create via {@link LogConfig} or directly: + *

{@code
+ * Logger logger = new Logger(LogLevel.DEBUG, new ConsoleLogger(), false);
+ * logger.debug("request sent");
+ * }
+ */ +public final class Logger { + + private static final Logger DEFAULT = new Logger(LogLevel.INFO, new ConsoleLogger(), true); + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + public Logger(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + /** + * Returns a default silent logger (no output). + */ + public static Logger getDefault() { + return DEFAULT; + } + + /** + * Creates a Logger from a {@link LogConfig}. If config is {@code null}, returns the default silent logger. + */ + public static Logger from(LogConfig config) { + if (config == null) { + return DEFAULT; + } + return new Logger(config.level(), config.logger(), config.silent()); + } + + /** + * Creates a Logger from an {@code Optional}. If empty, returns the default silent logger. + */ + public static Logger from(java.util.Optional config) { + return config.map(Logger::from).orElse(DEFAULT); + } + + private boolean shouldLog(LogLevel messageLevel) { + return !silent && level.getValue() <= messageLevel.getValue(); + } + + public boolean isDebug() { + return shouldLog(LogLevel.DEBUG); + } + + public boolean isInfo() { + return shouldLog(LogLevel.INFO); + } + + public boolean isWarn() { + return shouldLog(LogLevel.WARN); + } + + public boolean isError() { + return shouldLog(LogLevel.ERROR); + } + + public void debug(String message) { + if (isDebug()) { + logger.debug(message); + } + } + + public void info(String message) { + if (isInfo()) { + logger.info(message); + } + } + + public void warn(String message) { + if (isWarn()) { + logger.warn(message); + } + } + + public void error(String message) { + if (isError()) { + logger.error(message); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java new file mode 100644 index 000000000000..51073da7b3f0 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/LoggingInterceptor.java @@ -0,0 +1,104 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * OkHttp interceptor that logs HTTP requests and responses. + * + *

Logs request method, URL, and headers (with sensitive values redacted) at debug level. + * Logs response status at debug level, and 4xx/5xx responses at error level. + * Does nothing if the logger is silent. + */ +public final class LoggingInterceptor implements Interceptor { + + private static final Set SENSITIVE_HEADERS = new HashSet<>(Arrays.asList( + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "proxy-authenticate", + "proxy-authorization", + "cookie", + "set-cookie", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token")); + + private final Logger logger; + + public LoggingInterceptor(Logger logger) { + this.logger = logger; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Request: ").append(request.method()).append(" ").append(request.url()); + sb.append(" headers={"); + boolean first = true; + for (String name : request.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(request.header(name)); + } + first = false; + } + sb.append("}"); + sb.append(" has_body=").append(request.body() != null); + logger.debug(sb.toString()); + } + + Response response = chain.proceed(request); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Response: status=").append(response.code()); + sb.append(" url=").append(response.request().url()); + sb.append(" headers={"); + boolean first = true; + for (String name : response.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(response.header(name)); + } + first = false; + } + sb.append("}"); + logger.debug(sb.toString()); + } + + if (response.code() >= 400 && logger.isError()) { + logger.error("HTTP Error: status=" + response.code() + " url=" + + response.request().url()); + } + + return response; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java new file mode 100644 index 000000000000..15800a91a218 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java new file mode 100644 index 000000000000..4f93c2b696b7 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Nullable.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.Optional; +import java.util.function.Function; + +public final class Nullable { + + private final Either, Null> value; + + private Nullable() { + this.value = Either.left(Optional.empty()); + } + + private Nullable(T value) { + if (value == null) { + this.value = Either.right(Null.INSTANCE); + } else { + this.value = Either.left(Optional.of(value)); + } + } + + public static Nullable ofNull() { + return new Nullable<>(null); + } + + public static Nullable of(T value) { + return new Nullable<>(value); + } + + public static Nullable empty() { + return new Nullable<>(); + } + + public static Nullable ofOptional(Optional value) { + if (value.isPresent()) { + return of(value.get()); + } else { + return empty(); + } + } + + public boolean isNull() { + return this.value.isRight(); + } + + public boolean isEmpty() { + return this.value.isLeft() && !this.value.getLeft().isPresent(); + } + + public T get() { + if (this.isNull()) { + return null; + } + + return this.value.getLeft().get(); + } + + public Nullable map(Function mapper) { + if (this.isNull()) { + return Nullable.ofNull(); + } + + return Nullable.ofOptional(this.value.getLeft().map(mapper)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Nullable)) { + return false; + } + + if (((Nullable) other).isNull() && this.isNull()) { + return true; + } + + return this.value.getLeft().equals(((Nullable) other).value.getLeft()); + } + + private static final class Either { + private L left = null; + private R right = null; + + private Either(L left, R right) { + if (left != null && right != null) { + throw new IllegalArgumentException("Left and right argument cannot both be non-null."); + } + + if (left == null && right == null) { + throw new IllegalArgumentException("Left and right argument cannot both be null."); + } + + if (left != null) { + this.left = left; + } + + if (right != null) { + this.right = right; + } + } + + public static Either left(L left) { + return new Either<>(left, null); + } + + public static Either right(R right) { + return new Either<>(null, right); + } + + public boolean isLeft() { + return this.left != null; + } + + public boolean isRight() { + return this.right != null; + } + + public L getLeft() { + if (!this.isLeft()) { + throw new IllegalArgumentException("Cannot get left from right Either."); + } + return this.left; + } + + public R getRight() { + if (!this.isRight()) { + throw new IllegalArgumentException("Cannot get right from left Either."); + } + return this.right; + } + } + + private static final class Null { + private static final Null INSTANCE = new Null(); + + private Null() {} + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java new file mode 100644 index 000000000000..13b5f8a83bd1 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/NullableNonemptyFilter.java @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.Optional; + +public final class NullableNonemptyFilter { + @Override + public boolean equals(Object o) { + boolean isOptionalEmpty = isOptionalEmpty(o); + + return isOptionalEmpty; + } + + private boolean isOptionalEmpty(Object o) { + if (o instanceof Optional) { + return !((Optional) o).isPresent(); + } + return false; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java new file mode 100644 index 000000000000..9ffca6ae32f7 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ObjectMappers.java @@ -0,0 +1,46 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .addModule(DoubleSerializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } + + public static Object parseErrorBody(String responseBodyString) { + try { + return JSON_MAPPER.readValue(responseBodyString, Object.class); + } catch (JsonProcessingException ignored) { + return responseBodyString; + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java new file mode 100644 index 000000000000..929d95f5bb25 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/QueryStringMapper.java @@ -0,0 +1,142 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; + +public class QueryStringMapper { + + private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; + + public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + httpUrl.addQueryParameter(key, valueNode.textValue()); + } else { + httpUrl.addQueryParameter(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue()); + } else { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString()); + } + } + } + + public static void addFormDataPart( + MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + multipartBody.addFormDataPart(key, valueNode.textValue()); + } else { + multipartBody.addFormDataPart(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().textValue()); + } else { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().toString()); + } + } + } + + public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator> fields = object.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + + String key = "[" + field.getKey() + "]"; + + if (field.getValue().isObject()) { + List> flatField = + flattenObject((ObjectNode) field.getValue(), arraysAsRepeats); + addAll(flat, flatField, key); + } else if (field.getValue().isArray()) { + List> flatField = + flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats); + addAll(flat, flatField, ""); + } else { + flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue())); + } + } + + return flat; + } + + private static List> flattenArray( + ArrayNode array, String key, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator elements = array.elements(); + + int index = 0; + while (elements.hasNext()) { + JsonNode element = elements.next(); + + String indexKey = key + "[" + index + "]"; + + if (arraysAsRepeats) { + indexKey = key; + } + + if (element.isObject()) { + List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else if (element.isArray()) { + List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else { + flat.add(new AbstractMap.SimpleEntry<>(indexKey, element)); + } + + index++; + } + + return flat; + } + + private static void addAll( + List> target, List> source, String prefix) { + for (Map.Entry entry : source) { + Map.Entry entryToAdd = + new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue()); + target.add(entryToAdd); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java new file mode 100644 index 000000000000..008731e6b4ec --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RequestOptions.java @@ -0,0 +1,154 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class RequestOptions { + private final String token; + + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Optional maxRetries; + + private final Map headers; + + private final Map> headerSuppliers; + + private final Map queryParameters; + + private final Map> queryParameterSuppliers; + + private RequestOptions( + String token, + Optional timeout, + TimeUnit timeoutTimeUnit, + Optional maxRetries, + Map headers, + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { + this.token = token; + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.maxRetries = maxRetries; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Optional getMaxRetries() { + return maxRetries; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + if (this.token != null) { + headers.put("Authorization", "Bearer " + this.token); + } + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public Map getQueryParameters() { + Map queryParameters = new HashMap<>(this.queryParameters); + this.queryParameterSuppliers.forEach((key, supplier) -> { + queryParameters.put(key, supplier.get()); + }); + return queryParameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String token = null; + + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private Optional maxRetries = Optional.empty(); + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private final Map queryParameters = new HashMap<>(); + + private final Map> queryParameterSuppliers = new HashMap<>(); + + public Builder token(String token) { + this.token = token; + return this; + } + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, String value) { + this.queryParameters.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, Supplier value) { + this.queryParameterSuppliers.put(key, value); + return this; + } + + public RequestOptions build() { + return new RequestOptions( + token, + timeout, + timeoutTimeUnit, + maxRetries, + headers, + headerSuppliers, + queryParameters, + queryParameterSuppliers); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java new file mode 100644 index 000000000000..8574af21232c --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyInputStream.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.FilterInputStream; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java new file mode 100644 index 000000000000..654d2e879f6d --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseBodyReader.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.FilterReader; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java new file mode 100644 index 000000000000..4417ea04c51a --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/ResponseDecompressionInterceptor.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.IOException; +import java.util.zip.Inflater; +import okhttp3.Interceptor; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.GzipSource; +import okio.InflaterSource; +import okio.Okio; +import okio.Source; + +/** + * Decompresses gzip and deflate encoded response bodies. OkHttp only performs + * transparent decompression when it adds the Accept-Encoding header itself, so + * responses to requests with an explicitly set Accept-Encoding header would + * otherwise be returned as raw compressed bytes. + */ +public class ResponseDecompressionInterceptor implements Interceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + return decompress(chain.proceed(chain.request())); + } + + private Response decompress(Response response) { + ResponseBody body = response.body(); + if (body == null) { + return response; + } + String encoding = response.header("Content-Encoding"); + if (encoding == null) { + return response; + } + Source decompressedSource; + if (encoding.equalsIgnoreCase("gzip") || encoding.equalsIgnoreCase("x-gzip")) { + decompressedSource = new GzipSource(body.source()); + } else if (encoding.equalsIgnoreCase("deflate")) { + decompressedSource = new InflaterSource(body.source(), new Inflater()); + } else { + return response; + } + ResponseBody decompressedBody = ResponseBody.create(body.contentType(), -1L, Okio.buffer(decompressedSource)); + return response.newBuilder() + .headers(response.headers() + .newBuilder() + .removeAll("Content-Encoding") + .removeAll("Content-Length") + .build()) + .body(decompressedBody) + .build(); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java new file mode 100644 index 000000000000..622db74b15e0 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/RetryInterceptor.java @@ -0,0 +1,242 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.Random; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor { + + private static final Duration DEFAULT_INITIAL_RETRY_DELAY = Duration.ofMillis(1000); + private static final Duration DEFAULT_MAX_RETRY_DELAY = Duration.ofMillis(60000); + private static final double DEFAULT_JITTER_FACTOR = 0.2; + + private final int maxRetries; + private final Duration initialRetryDelay; + private final Duration maxRetryDelay; + private final double jitterFactor; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this(maxRetries, Optional.empty(), Optional.empty(), Optional.empty()); + } + + public RetryInterceptor( + int maxRetries, + Optional initialRetryDelayMillis, + Optional maxRetryDelayMillis, + Optional jitterFactor) { + initialRetryDelayMillis.ifPresent(delay -> { + if (delay < 0) { + throw new IllegalArgumentException("initialRetryDelayMillis must be non-negative"); + } + }); + maxRetryDelayMillis.ifPresent(delay -> { + if (delay < 0) { + throw new IllegalArgumentException("maxRetryDelayMillis must be non-negative"); + } + }); + jitterFactor.ifPresent(factor -> { + if (factor < 0 || factor > 1) { + throw new IllegalArgumentException("jitterFactor must be between 0 and 1"); + } + }); + this.maxRetries = maxRetries; + this.initialRetryDelay = initialRetryDelayMillis.map(Duration::ofMillis).orElse(DEFAULT_INITIAL_RETRY_DELAY); + this.maxRetryDelay = maxRetryDelayMillis.map(Duration::ofMillis).orElse(DEFAULT_MAX_RETRY_DELAY); + this.jitterFactor = jitterFactor.orElse(DEFAULT_JITTER_FACTOR); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + int effectiveMaxRetries = resolveMaxRetries(request); + Response response = chain.proceed(request); + + if (shouldRetry(response.code())) { + return retryChain(response, chain, effectiveMaxRetries); + } + + return response; + } + + private int resolveMaxRetries(Request request) { + MaxRetriesOverride override = request.tag(MaxRetriesOverride.class); + return override != null ? override.getValue() : this.maxRetries; + } + + private Response retryChain(Response response, Chain chain, int maxRetries) throws IOException { + ExponentialBackoff backoff = new ExponentialBackoff(maxRetries); + Optional nextBackoff = backoff.nextBackoff(response); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = backoff.nextBackoff(response); + } else { + return response; + } + } + + return response; + } + + /** + * Calculates the retry delay from response headers, with fallback to exponential backoff. + * Priority: Retry-After > X-RateLimit-Reset > Exponential Backoff + */ + private Duration getRetryDelayFromHeaders(Response response, int retryAttempt) { + // Check for Retry-After header first (RFC 7231), with no jitter + String retryAfter = response.header("Retry-After"); + if (retryAfter != null) { + // Parse as number of seconds... + Optional secondsDelay = tryParseLong(retryAfter) + .map(seconds -> seconds * 1000) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(Duration::ofMillis); + if (secondsDelay.isPresent()) { + return secondsDelay.get(); + } + + // ...or as an HTTP date; both are valid + Optional dateDelay = tryParseHttpDate(retryAfter) + .map(resetTime -> resetTime.toInstant().toEpochMilli() - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(Duration::ofMillis); + if (dateDelay.isPresent()) { + return dateDelay.get(); + } + } + + // Then check for industry-standard X-RateLimit-Reset header, with positive jitter + String rateLimitReset = response.header("X-RateLimit-Reset"); + if (rateLimitReset != null) { + // Assume Unix timestamp in epoch seconds + Optional rateLimitDelay = tryParseLong(rateLimitReset) + .map(resetTimeSeconds -> (resetTimeSeconds * 1000) - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(this::addPositiveJitter) + .map(Duration::ofMillis); + if (rateLimitDelay.isPresent()) { + return rateLimitDelay.get(); + } + } + + // Fall back to exponential backoff, with symmetric jitter + long initialDelayMillis = initialRetryDelay.toMillis(); + long maxDelayMillis = maxRetryDelay.toMillis(); + long cappedDelay; + if (retryAttempt >= Long.SIZE - 1 || initialDelayMillis > (maxDelayMillis >> retryAttempt)) { + // initialDelayMillis * 2^retryAttempt would exceed maxDelayMillis (or overflow) + cappedDelay = maxDelayMillis; + } else { + cappedDelay = Math.min(initialDelayMillis << retryAttempt, maxDelayMillis); // 2^retryAttempt + } + return Duration.ofMillis(addSymmetricJitter(cappedDelay)); + } + + /** + * Attempts to parse a string as a long, returning empty Optional on failure. + */ + private Optional tryParseLong(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + /** + * Attempts to parse a string as an HTTP date (RFC 1123), returning empty Optional on failure. + */ + private Optional tryParseHttpDate(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + /** + * Adds positive jitter (100-120% of original value) to prevent thundering herd. + * Used for X-RateLimit-Reset header delays. + */ + private long addPositiveJitter(long delayMs) { + double jitterMultiplier = 1.0 + (random.nextDouble() * jitterFactor); + return (long) (delayMs * jitterMultiplier); + } + + /** + * Adds symmetric jitter (90-110% of original value) to prevent thundering herd. + * Used for exponential backoff delays. + */ + private long addSymmetricJitter(long delayMs) { + double jitterMultiplier = 1.0 + ((random.nextDouble() - 0.5) * jitterFactor); + return (long) (delayMs * jitterMultiplier); + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + /** + * Per-request override carried on the OkHttp {@link Request} as a tag. + * When present, the interceptor uses this value instead of the client-wide + * {@code maxRetries} configured at construction time. + */ + public static final class MaxRetriesOverride { + private final int value; + + public MaxRetriesOverride(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff(Response response) { + if (retryNumber >= maxNumRetries) { + return Optional.empty(); + } + + Duration delay = getRetryDelayFromHeaders(response, retryNumber); + retryNumber += 1; + return Optional.of(delay); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java new file mode 100644 index 000000000000..ae98a6a8fea5 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Rfc2822DateTimeDeserializer.java @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Custom deserializer that handles converting RFC 2822 (RFC 1123) dates into {@link OffsetDateTime} objects. + * This is used for fields with format "date-time-rfc-2822", such as Twilio's dateCreated, dateSent, dateUpdated. + */ +public class Rfc2822DateTimeDeserializer extends JsonDeserializer { + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + String raw = parser.getValueAsString(); + return ZonedDateTime.parse(raw, DateTimeFormatter.RFC_1123_DATE_TIME).toOffsetDateTime(); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java new file mode 100644 index 000000000000..42d1b0980390 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersApiException.java @@ -0,0 +1,73 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class SeedIdempotencyHeadersApiException extends SeedIdempotencyHeadersException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + private final Map> headers; + + public SeedIdempotencyHeadersApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + } + + public SeedIdempotencyHeadersApiException(String message, int statusCode, Object body, Response rawResponse) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + /** + * @return the headers + */ + public Map> headers() { + return this.headers; + } + + @Override + public String toString() { + return "SeedIdempotencyHeadersApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + + ", body: " + ObjectMappers.stringify(body) + "}"; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java new file mode 100644 index 000000000000..009f67fca341 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class SeedIdempotencyHeadersException extends RuntimeException { + public SeedIdempotencyHeadersException(String message) { + super(message); + } + + public SeedIdempotencyHeadersException(String message, Exception e) { + super(message, e); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java new file mode 100644 index 000000000000..b939c61e8f62 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SeedIdempotencyHeadersHttpResponse.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +public final class SeedIdempotencyHeadersHttpResponse { + + private final T body; + + private final Map> headers; + + public SeedIdempotencyHeadersHttpResponse(T body, Response rawResponse) { + this.body = body; + + Map> headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + this.headers = headers; + } + + public T body() { + return this.body; + } + + public Map> headers() { + return headers; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java new file mode 100644 index 000000000000..a964cedcc005 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEvent.java @@ -0,0 +1,114 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a Server-Sent Event with all standard fields. + * Used for event-level discrimination where the discriminator is at the SSE envelope level. + * + * @param The type of the data field + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SseEvent { + private final String event; + private final T data; + private final String id; + private final Long retry; + + private SseEvent(String event, T data, String id, Long retry) { + this.event = event; + this.data = data; + this.id = id; + this.retry = retry; + } + + @JsonProperty("event") + public Optional getEvent() { + return Optional.ofNullable(event); + } + + @JsonProperty("data") + public T getData() { + return data; + } + + @JsonProperty("id") + public Optional getId() { + return Optional.ofNullable(id); + } + + @JsonProperty("retry") + public Optional getRetry() { + return Optional.ofNullable(retry); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SseEvent sseEvent = (SseEvent) o; + return Objects.equals(event, sseEvent.event) + && Objects.equals(data, sseEvent.data) + && Objects.equals(id, sseEvent.id) + && Objects.equals(retry, sseEvent.retry); + } + + @Override + public int hashCode() { + return Objects.hash(event, data, id, retry); + } + + @Override + public String toString() { + return "SseEvent{" + "event='" + + event + '\'' + ", data=" + + data + ", id='" + + id + '\'' + ", retry=" + + retry + '}'; + } + + public static Builder builder() { + return new Builder<>(); + } + + public static final class Builder { + private String event; + private T data; + private String id; + private Long retry; + + private Builder() {} + + public Builder event(String event) { + this.event = event; + return this; + } + + public Builder data(T data) { + this.data = data; + return this; + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder retry(Long retry) { + this.retry = retry; + return this; + } + + public SseEvent build() { + return new SseEvent<>(event, data, id, retry); + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java new file mode 100644 index 000000000000..52e6ee31a581 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/SseEventParser.java @@ -0,0 +1,228 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.type.TypeReference; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Utility class for parsing Server-Sent Events with support for discriminated unions. + *

+ * Handles two discrimination patterns: + *

    + *
  1. Data-level discrimination: The discriminator (e.g., 'type') is inside the JSON data payload. + * Jackson's polymorphic deserialization handles this automatically.
  2. + *
  3. Event-level discrimination: The discriminator (e.g., 'event') is at the SSE envelope level. + * This requires constructing the full SSE envelope for Jackson to process.
  4. + *
+ */ +public final class SseEventParser { + + private static final Set SSE_ENVELOPE_FIELDS = new HashSet<>(Arrays.asList("event", "data", "id", "retry")); + + private SseEventParser() { + // Utility class + } + + /** + * Parse an SSE event using event-level discrimination. + *

+ * Constructs the full SSE envelope object with event, data, id, and retry fields, + * then deserializes it to the target union type. + * + * @param eventType The SSE event type (from event: field) + * @param data The SSE data content (from data: field) + * @param id The SSE event ID (from id: field), may be null + * @param retry The SSE retry value (from retry: field), may be null + * @param unionClass The target union class + * @param discriminatorProperty The property name used for discrimination (e.g., "event") + * @param The target type + * @return The deserialized object + */ + public static T parseEventLevelUnion( + String eventType, String data, String id, Long retry, Class unionClass, String discriminatorProperty) { + try { + // Determine if data should be parsed as JSON based on the variant's expected type + Object parsedData = parseDataForVariant(eventType, data, unionClass, discriminatorProperty); + + // Construct the SSE envelope object + Map envelope = new HashMap<>(); + envelope.put(discriminatorProperty, eventType); + envelope.put("data", parsedData); + if (id != null) { + envelope.put("id", id); + } + if (retry != null) { + envelope.put("retry", retry); + } + + // Serialize to JSON and deserialize to target type + String envelopeJson = ObjectMappers.JSON_MAPPER.writeValueAsString(envelope); + return ObjectMappers.JSON_MAPPER.readValue(envelopeJson, unionClass); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE event with event-level discrimination", e); + } + } + + /** + * Parse an SSE event using data-level discrimination. + *

+ * Simply parses the data field as JSON and deserializes it to the target type. + * Jackson's polymorphic deserialization handles the discrimination automatically. + * + * @param data The SSE data content (from data: field) + * @param valueType The target type + * @param The target type + * @return The deserialized object + */ + public static T parseDataLevelUnion(String data, Class valueType) { + try { + return ObjectMappers.JSON_MAPPER.readValue(data, valueType); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE data with data-level discrimination", e); + } + } + + /** + * Determines if the given discriminator property indicates event-level discrimination. + * Event-level discrimination occurs when the discriminator is an SSE envelope field. + * + * @param discriminatorProperty The discriminator property name + * @return true if event-level discrimination, false otherwise + */ + public static boolean isEventLevelDiscrimination(String discriminatorProperty) { + return SSE_ENVELOPE_FIELDS.contains(discriminatorProperty); + } + + /** + * Attempts to find the discriminator property from the union class's Jackson annotations. + * + * @param unionClass The union class to inspect + * @return The discriminator property name, or empty if not found + */ + public static Optional findDiscriminatorProperty(Class unionClass) { + try { + // Look for JsonTypeInfo on the class itself + JsonTypeInfo typeInfo = unionClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + + // Look for inner Value interface with JsonTypeInfo + for (Class innerClass : unionClass.getDeclaredClasses()) { + typeInfo = innerClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return Optional.empty(); + } + + /** + * Parse the data field based on what the matching variant expects. + * If the variant expects a String for its data field, returns the raw string. + * Otherwise, parses the data as JSON. + */ + private static Object parseDataForVariant( + String eventType, String data, Class unionClass, String discriminatorProperty) { + if (data == null || data.isEmpty()) { + return data; + } + + try { + // Try to find the variant class that matches this event type + Class variantClass = findVariantClass(unionClass, eventType, discriminatorProperty); + if (variantClass != null) { + // Check if the variant expects a String for the data field + Field dataField = findField(variantClass, "data"); + if (dataField != null && String.class.equals(dataField.getType())) { + // Variant expects String - return raw data + return data; + } + } + + // Try to parse as JSON + return ObjectMappers.JSON_MAPPER.readValue(data, new TypeReference>() {}); + } catch (Exception e) { + // If JSON parsing fails, return as string + return data; + } + } + + /** + * Find the variant class that matches the given discriminator value. + */ + private static Class findVariantClass( + Class unionClass, String discriminatorValue, String discriminatorProperty) { + try { + // Look for JsonSubTypes annotation + JsonSubTypes subTypes = findJsonSubTypes(unionClass); + if (subTypes == null) { + return null; + } + + for (JsonSubTypes.Type subType : subTypes.value()) { + JsonTypeName typeName = subType.value().getAnnotation(JsonTypeName.class); + if (typeName != null && typeName.value().equals(discriminatorValue)) { + return subType.value(); + } + // Also check the name attribute of @JsonSubTypes.Type + if (subType.name().equals(discriminatorValue)) { + return subType.value(); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return null; + } + + /** + * Find JsonSubTypes annotation on the class or its inner classes. + */ + private static JsonSubTypes findJsonSubTypes(Class unionClass) { + // Check the class itself + JsonSubTypes subTypes = unionClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + + // Check inner classes (for Fern-style unions with inner Value interface) + for (Class innerClass : unionClass.getDeclaredClasses()) { + subTypes = innerClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + } + return null; + } + + /** + * Find a field by name in a class, including private fields. + */ + private static Field findField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // Check superclass + Class superClass = clazz.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return findField(superClass, fieldName); + } + return null; + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Stream.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Stream.java new file mode 100644 index 000000000000..c1fb050caf78 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Stream.java @@ -0,0 +1,513 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * Iterable stream for parsing JSON and Server-Sent Events (SSE) data. + * Supports both newline-delimited JSON and SSE with optional stream termination. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable, Closeable { + + private static final String NEWLINE = "\n"; + private static final String DATA_PREFIX = "data:"; + + public enum StreamType { + JSON, + SSE, + SSE_EVENT_DISCRIMINATED + } + + private final Class valueType; + private final Scanner scanner; + private final StreamType streamType; + private final String messageTerminator; + private final String streamTerminator; + private final Reader sseReader; + private final String discriminatorProperty; + private boolean isClosed = false; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.valueType = valueType; + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.streamType = StreamType.JSON; + this.messageTerminator = delimiter; + this.streamTerminator = null; + this.sseReader = null; + this.discriminatorProperty = null; + } + + private Stream(Class valueType, StreamType type, Reader reader, String terminator) { + this(valueType, type, reader, terminator, null); + } + + private Stream( + Class valueType, StreamType type, Reader reader, String terminator, String discriminatorProperty) { + this.valueType = valueType; + this.streamType = type; + this.discriminatorProperty = discriminatorProperty; + if (type == StreamType.JSON) { + this.scanner = new Scanner(reader).useDelimiter(terminator); + this.messageTerminator = terminator; + this.streamTerminator = null; + this.sseReader = null; + } else { + this.scanner = null; + this.messageTerminator = NEWLINE; + this.streamTerminator = terminator; + this.sseReader = reader; + } + } + + public static Stream fromJson(Class valueType, Reader reader, String delimiter) { + return new Stream<>(valueType, reader, delimiter); + } + + public static Stream fromJson(Class valueType, Reader reader) { + return new Stream<>(valueType, reader, NEWLINE); + } + + public static Stream fromSse(Class valueType, Reader sseReader) { + return new Stream<>(valueType, StreamType.SSE, sseReader, null); + } + + public static Stream fromSse(Class valueType, Reader sseReader, String streamTerminator) { + return new Stream<>(valueType, StreamType.SSE, sseReader, streamTerminator); + } + + /** + * Creates a stream from SSE data with event-level discrimination support. + * Use this when the SSE payload is a discriminated union where the discriminator + * is an SSE envelope field (e.g., 'event'). + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty) { + return new Stream<>(valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, null, discriminatorProperty); + } + + /** + * Creates a stream from SSE data with event-level discrimination support and a stream terminator. + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param streamTerminator The terminator string that signals end of stream (e.g., "[DONE]"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty, String streamTerminator) { + return new Stream<>( + valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, streamTerminator, discriminatorProperty); + } + + @Override + public void close() throws IOException { + if (!isClosed) { + isClosed = true; + if (scanner != null) { + scanner.close(); + } + if (sseReader != null) { + sseReader.close(); + } + } + } + + private boolean isStreamClosed() { + return isClosed; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + switch (streamType) { + case SSE: + return new SSEIterator(); + case SSE_EVENT_DISCRIMINATED: + return new SSEEventDiscriminatedIterator(); + case JSON: + default: + return new JsonIterator(); + } + } + + private final class JsonIterator implements Iterator { + + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + if (isStreamClosed()) { + return false; + } + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (isStreamClosed()) { + throw new NoSuchElementException("Stream is closed"); + } + + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = + ObjectMappers.JSON_MAPPER.readValue(scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private final class SSEIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + + private SSEIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + // Event ID field (ignored) + } else if (line.startsWith("retry:")) { + // Retry field (ignored) + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + } + + /** + * Iterator for SSE streams with event-level discrimination. + * Uses SseEventParser to construct the full SSE envelope for Jackson deserialization. + */ + private final class SSEEventDiscriminatedIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + private String currentEventId = null; + private Long currentRetry = null; + + private SSEEventDiscriminatedIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + // Use SseEventParser for event-level discrimination + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + resetEventState(); + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + String idValue = line.length() > 3 ? line.substring(3) : ""; + if (idValue.startsWith(" ")) { + idValue = idValue.substring(1); + } + currentEventId = idValue; + } else if (line.startsWith("retry:")) { + String retryValue = line.length() > 6 ? line.substring(6) : ""; + if (retryValue.startsWith(" ")) { + retryValue = retryValue.substring(1); + } + try { + currentRetry = Long.parseLong(retryValue.trim()); + } catch (NumberFormatException e) { + // Ignore invalid retry values + } + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + // Handle any remaining buffered data at end of stream + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + resetEventState(); + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + + private void resetEventState() { + eventDataBuffer.setLength(0); + currentEventType = null; + currentEventId = null; + currentRetry = null; + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java new file mode 100644 index 000000000000..566182a8af16 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java new file mode 100644 index 000000000000..16c964456798 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncPaymentClient.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.IdempotentRequestOptions; +import com.seed.idempotencyHeaders.core.RequestOptions; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class AsyncPaymentClient { + protected final ClientOptions clientOptions; + + private final AsyncRawPaymentClient rawClient; + + public AsyncPaymentClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawPaymentClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawPaymentClient withRawResponse() { + return this.rawClient; + } + + public CompletableFuture create(CreatePaymentRequest request) { + return this.rawClient.create(request).thenApply(response -> response.body()); + } + + public CompletableFuture create(CreatePaymentRequest request, IdempotentRequestOptions requestOptions) { + return this.rawClient.create(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture delete(String paymentId) { + return this.rawClient.delete(paymentId).thenApply(response -> response.body()); + } + + public CompletableFuture delete(String paymentId, RequestOptions requestOptions) { + return this.rawClient.delete(paymentId, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java rename to seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/AsyncRawPaymentClient.java diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java new file mode 100644 index 000000000000..d94ae98a6202 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/PaymentClient.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment; + +import com.seed.idempotencyHeaders.core.ClientOptions; +import com.seed.idempotencyHeaders.core.IdempotentRequestOptions; +import com.seed.idempotencyHeaders.core.RequestOptions; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import java.util.UUID; + +public class PaymentClient { + protected final ClientOptions clientOptions; + + private final RawPaymentClient rawClient; + + public PaymentClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawPaymentClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawPaymentClient withRawResponse() { + return this.rawClient; + } + + public UUID create(CreatePaymentRequest request) { + return this.rawClient.create(request).body(); + } + + public UUID create(CreatePaymentRequest request, IdempotentRequestOptions requestOptions) { + return this.rawClient.create(request, requestOptions).body(); + } + + public void delete(String paymentId) { + this.rawClient.delete(paymentId).body(); + } + + public void delete(String paymentId, RequestOptions requestOptions) { + this.rawClient.delete(paymentId, requestOptions).body(); + } +} diff --git a/seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java similarity index 100% rename from seed/java-sdk/idempotency-headers/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java rename to seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/RawPaymentClient.java diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java new file mode 100644 index 000000000000..b71fe867e08d --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/requests/CreatePaymentRequest.java @@ -0,0 +1,141 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.idempotencyHeaders.core.ObjectMappers; +import com.seed.idempotencyHeaders.resources.payment.types.Currency; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = CreatePaymentRequest.Builder.class) +public final class CreatePaymentRequest { + private final int amount; + + private final Currency currency; + + private final Map additionalProperties; + + private CreatePaymentRequest(int amount, Currency currency, Map additionalProperties) { + this.amount = amount; + this.currency = currency; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("amount") + public int getAmount() { + return amount; + } + + @JsonProperty("currency") + public Currency getCurrency() { + return currency; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreatePaymentRequest && equalTo((CreatePaymentRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(CreatePaymentRequest other) { + return amount == other.amount && currency.equals(other.currency); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.amount, this.currency); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static AmountStage builder() { + return new Builder(); + } + + public interface AmountStage { + CurrencyStage amount(int amount); + + Builder from(CreatePaymentRequest other); + } + + public interface CurrencyStage { + _FinalStage currency(@NotNull Currency currency); + } + + public interface _FinalStage { + CreatePaymentRequest build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements AmountStage, CurrencyStage, _FinalStage { + private int amount; + + private Currency currency; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(CreatePaymentRequest other) { + amount(other.getAmount()); + currency(other.getCurrency()); + return this; + } + + @java.lang.Override + @JsonSetter("amount") + public CurrencyStage amount(int amount) { + this.amount = amount; + return this; + } + + @java.lang.Override + @JsonSetter("currency") + public _FinalStage currency(@NotNull Currency currency) { + this.currency = Objects.requireNonNull(currency, "currency must not be null"); + return this; + } + + @java.lang.Override + public CreatePaymentRequest build() { + return new CreatePaymentRequest(amount, currency, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java new file mode 100644 index 000000000000..2cf32f741f74 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/seed/idempotencyHeaders/resources/payment/types/Currency.java @@ -0,0 +1,82 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.resources.payment.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class Currency { + public static final Currency YEN = new Currency(Value.YEN, "YEN"); + + public static final Currency USD = new Currency(Value.USD, "USD"); + + private final Value value; + + private final String string; + + Currency(Value value, String string) { + this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; + } + + @java.lang.Override + @JsonValue + public String toString() { + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) || (other instanceof Currency && this.string.equals(((Currency) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case YEN: + return visitor.visitYen(); + case USD: + return visitor.visitUsd(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static Currency valueOf(String value) { + switch (value) { + case "YEN": + return YEN; + case "USD": + return USD; + default: + return new Currency(Value.UNKNOWN, value); + } + } + + public enum Value { + USD, + + YEN, + + UNKNOWN + } + + public interface Visitor { + T visitUsd(); + + T visitYen(); + + T visitUnknown(String unknownType); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example0.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example0.java new file mode 100644 index 000000000000..b252f4d6b304 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example0.java @@ -0,0 +1,20 @@ +package com.snippets; + +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; +import com.seed.idempotencyHeaders.resources.payment.requests.CreatePaymentRequest; +import com.seed.idempotencyHeaders.resources.payment.types.Currency; + +public class Example0 { + public static void main(String[] args) { + SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.payment() + .create(CreatePaymentRequest.builder() + .amount(1) + .currency(Currency.USD) + .build()); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example1.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example1.java new file mode 100644 index 000000000000..8b6b389b1a9f --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/main/java/com/snippets/Example1.java @@ -0,0 +1,14 @@ +package com.snippets; + +import com.seed.idempotencyHeaders.SeedIdempotencyHeadersClient; + +public class Example1 { + public static void main(String[] args) { + SeedIdempotencyHeadersClient client = SeedIdempotencyHeadersClient.builder() + .token("") + .url("https://api.fern.com") + .build(); + + client.payment().delete("paymentId"); + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/StreamTest.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/StreamTest.java new file mode 100644 index 000000000000..2ad20377f845 --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/StreamTest.java @@ -0,0 +1,120 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +import static org.junit.jupiter.api.Assertions.*; + +import com.seed.idempotencyHeaders.core.ObjectMappers; +import com.seed.idempotencyHeaders.core.Stream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public final class StreamTest { + @Test + public void testJsonStream() { + List> messages = + Arrays.asList(createMap("message", "hello"), createMap("message", "world")); + List jsonStrings = messages.stream().map(StreamTest::mapToJson).collect(Collectors.toList()); + String input = String.join("\n", jsonStrings); + StringReader jsonInput = new StringReader(input); + Stream jsonStream = Stream.fromJson(Map.class, jsonInput); + int expectedMessages = 2; + int actualMessages = 0; + for (Map jsonObject : jsonStream) { + actualMessages++; + assertTrue(jsonObject.containsKey("message")); + } + assertEquals(expectedMessages, actualMessages); + } + + @Test + public void testSseStream() { + List> events = Arrays.asList(createMap("event", "start"), createMap("event", "end")); + List sseStrings = events.stream().map(StreamTest::mapToSse).collect(Collectors.toList()); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("event")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseStreamWithTerminator() { + List> events = Arrays.asList(createMap("message", "first"), createMap("message", "second")); + List sseStrings = + new ArrayList<>(events.stream().map(StreamTest::mapToSse).collect(Collectors.toList())); + sseStrings.add("data: [DONE]"); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput, "[DONE]"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("message")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseEventDiscriminatedStream() { + List sseStrings = Arrays.asList( + mapToSseWithEvent("start", createMap("status", "pending")), + mapToSseWithEvent("end", createMap("status", "complete"))); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSseWithEventDiscrimination(Map.class, sseInput, "event"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + // Event-level discrimination includes the event field in the parsed result + assertTrue(eventData.containsKey("event")); + assertTrue(eventData.containsKey("data")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testStreamResourceManagement() throws IOException { + StringReader testInput = new StringReader("{\"test\":\"data\"}"); + Stream testStream = Stream.fromJson(Map.class, testInput); + testStream.close(); + assertFalse(testStream.iterator().hasNext()); + } + + private static String mapToJson(Map map) { + try { + return ObjectMappers.JSON_MAPPER.writeValueAsString(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String mapToSse(Map map) { + return "data: " + mapToJson(map); + } + + private static String mapToSseWithEvent(String eventType, Map data) { + return "event: " + eventType + "\n" + "data: " + mapToJson(data); + } + + private static Map createMap(String key, String value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/TestClient.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/TestClient.java new file mode 100644 index 000000000000..0333c12fc51d --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/TestClient.java @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders; + +public final class TestClient { + public void test() { + // Add tests here and mark this file in .fernignore + assert true; + } +} diff --git a/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java new file mode 100644 index 000000000000..8e1abbb6bf0f --- /dev/null +++ b/seed/java-sdk/idempotency-headers/no-custom-config/src/test/java/com/seed/idempotencyHeaders/core/QueryStringMapperTest.java @@ -0,0 +1,339 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.idempotencyHeaders.core; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class QueryStringMapperTest { + @Test + public void testObjectWithQuotedString_indexedArrays() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithQuotedString_arraysAsRepeats() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_indexedArrays() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_arraysAsRepeats() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_indexedArrays() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_arraysAsRepeats() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_indexedArrays() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_arraysAsRepeats() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_indexedArrays() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = "objects%5B0%5D%5Bvalue%5D=world&objects%5B0%5D%5Bkey%5D=hello&objects%5B1%5D" + + "%5Bvalue%5D=bar&objects%5B1%5D%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_arraysAsRepeats() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = + "objects%5Bvalue%5D=world&objects%5Bkey%5D=hello&objects%5Bvalue" + "%5D=bar&objects%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_indexedArrays() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = + "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds%5D%5B0%5D=id1&objectwitharray" + + "%5BcontactIds%5D%5B1%5D=id2&objectwitharray%5BcontactIds%5D%5B2%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_arraysAsRepeats() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds" + + "%5D=id1&objectwitharray%5BcontactIds%5D=id2&objectwitharray%5BcontactIds%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + private static String queryString(Map params, boolean arraysAsRepeats) { + HttpUrl.Builder httpUrl = HttpUrl.parse("http://www.fakewebsite.com/").newBuilder(); + params.forEach((paramName, paramValue) -> + QueryStringMapper.addQueryParameter(httpUrl, paramName, paramValue, arraysAsRepeats)); + return httpUrl.build().encodedQuery(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.fern/metadata.json b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.fern/metadata.json new file mode 100644 index 000000000000..e788dc71dba2 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.fern/metadata.json @@ -0,0 +1,13 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-java-sdk", + "generatorVersion": "local", + "generatorConfig": { + "auto-generate-idempotency-key": true + }, + "originGitCommit": "DUMMY", + "invokedBy": "ci", + "requestedVersion": "0.0.1", + "ciProvider": "github", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.github/workflows/ci.yml b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.github/workflows/ci.yml new file mode 100644 index 000000000000..09c8c666ad73 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: ci + +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Compile + run: ./gradlew compileJava + + test: + needs: [ compile ] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Test + run: ./gradlew test + publish: + needs: [ compile, test ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + id: setup-jre + uses: actions/setup-java@v1 + with: + java-version: "11" + architecture: x64 + + - name: Publish to maven + run: | + ./gradlew publish + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLISH_REGISTRY_URL: "" diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.gitignore b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.gitignore new file mode 100644 index 000000000000..d4199abc2cd4 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/.gitignore @@ -0,0 +1,24 @@ +*.class +.project +.gradle +? +.classpath +.checkstyle +.settings +.node +build + +# IntelliJ +*.iml +*.ipr +*.iws +.idea/ +out/ + +# Eclipse/IntelliJ APT +generated_src/ +generated_testSrc/ +generated/ + +bin +build \ No newline at end of file diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/CONTRIBUTING.md b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/CONTRIBUTING.md new file mode 100644 index 000000000000..24cf9fa0c645 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Java 11+ +- Gradle + +### Installation + +Install the project dependencies: + +```bash +./gradlew build +``` + +### Building + +Build the project: + +```bash +./gradlew build +``` + +### Testing + +Run the test suite: + +```bash +./gradlew test +``` + +### Formatting + +Check and fix code style: + +```bash +./gradlew spotlessApply +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Java files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Java SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/java-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `./gradlew test` +4. Run formatting: `./gradlew spotlessApply` +5. Build the project: `./gradlew build` +6. Commit your changes with a clear commit message +7. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting. Run `./gradlew spotlessApply` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/README.md b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/README.md new file mode 100644 index 000000000000..59d8357a6281 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/README.md @@ -0,0 +1,217 @@ +# Seed Java 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%2FJava) +[![Maven Central](https://img.shields.io/maven-central/v/com.fern/java-idempotency-headers-file-upload)](https://central.sonatype.com/artifact/com.fern/java-idempotency-headers-file-upload) + +The Seed Java library provides convenient access to the Seed APIs from Java. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Base Url](#base-url) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Custom Client](#custom-client) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Headers](#custom-headers) + - [Access Raw Response Data](#access-raw-response-data) +- [Contributing](#contributing) + +## Installation + +### Gradle + +Add the dependency in your `build.gradle` file: + +```groovy +dependencies { + implementation 'com.fern:java-idempotency-headers-file-upload:0.0.1' +} +``` + +### Maven + +Add the dependency in your `pom.xml` file: + +```xml + + com.fern + java-idempotency-headers-file-upload + 0.0.1 + +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```java +package com.example.usage; + +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; + +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .token("") + .build(); + +client.service().upload(...); +``` + +## Base Url + +You can set a custom base URL when constructing the client. + +```java +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; + +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .url("https://example.com") + .build(); +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), an API exception will be thrown. + +```java +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadApiException; + +try{ + client.service().upload(...); +} catch (SeedJavaIdempotencyHeadersFileUploadApiException e){ + // Do something with the API exception... +} +``` + +## Advanced + +### Custom Client + +This SDK is built to work with any instance of `OkHttpClient`. By default, if no client is provided, the SDK will construct one. +However, you can pass your own client like so: + +```java +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; +import okhttp3.OkHttpClient; + +OkHttpClient customClient = ...; + +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .httpClient(customClient) + .build(); +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). Before defaulting to exponential backoff, the SDK will first attempt to respect +the `Retry-After` header (as either in seconds or as an HTTP date), and then the `X-RateLimit-Reset` header +(as a Unix timestamp in epoch seconds); failing both of those, it will fall back to exponential backoff. + +Which status codes are retried depends on the `retry-status-codes` generator configuration: + +**`legacy`** (current default): retries on +- [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#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [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) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `maxRetries` client option to configure this behavior. + +```java +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; + +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .maxRetries(1) + .build(); +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. +```java +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; + +// Client level +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .timeout(60) + .build(); + +// Request level +client.service().upload( + ..., + RequestOptions + .builder() + .timeout(60) + .build() +); +``` + +### Custom Headers + +The SDK allows you to add custom headers to requests. You can configure headers at the client level or at the request level. + +```java +import com.seed.javaIdempotencyHeadersFileUpload.SeedJavaIdempotencyHeadersFileUploadClient; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; + +// Client level +SeedJavaIdempotencyHeadersFileUploadClient client = SeedJavaIdempotencyHeadersFileUploadClient + .builder() + .addHeader("X-Custom-Header", "custom-value") + .addHeader("X-Request-Id", "abc-123") + .build(); +; + +// Request level +client.service().upload( + ..., + RequestOptions + .builder() + .addHeader("X-Request-Header", "request-value") + .build() +); +``` + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `withRawResponse()` method. +The `withRawResponse()` method returns a raw client that wraps all responses with `body()` and `headers()` methods. +(A normal client's `response` is identical to a raw client's `response.body()`.) + +```java +SeedJavaIdempotencyHeadersFileUploadHttpResponse response = client.service().withRawResponse().upload(...); + +System.out.println(response.body()); +System.out.println(response.headers().get("X-My-Header")); +``` + +## 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/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/build.gradle b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/build.gradle new file mode 100644 index 000000000000..056ff2830262 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/build.gradle @@ -0,0 +1,102 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.11.0' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + api 'com.squareup.okhttp3:okhttp:5.2.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.18.6' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.6' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.6' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +tasks.withType(Javadoc) { + failOnError false + options.addStringOption('Xdoclint:none', '-quiet') +} + +spotless { + java { + palantirJavaFormat() + } +} + + +java { + withSourcesJar() + withJavadocJar() +} + + +group = 'com.fern' + +version = '0.0.1' + +jar { + dependsOn(":generatePomFileForMavenPublication") + archiveBaseName = "java-idempotency-headers-file-upload" +} + +sourcesJar { + archiveBaseName = "java-idempotency-headers-file-upload" +} + +javadocJar { + archiveBaseName = "java-idempotency-headers-file-upload" +} + +test { + useJUnitPlatform() + testLogging { + showStandardStreams = true + } +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'com.fern' + artifactId = 'java-idempotency-headers-file-upload' + version = '0.0.1' + from components.java + pom { + licenses { + license { + name = 'The MIT License (MIT)' + url = 'https://mit-license.org/' + } + } + scm { + connection = 'scm:git:git://github.com/java-idempotency-headers-file-upload/fern.git' + developerConnection = 'scm:git:git://github.com/java-idempotency-headers-file-upload/fern.git' + url = 'https://github.com/java-idempotency-headers-file-upload/fern' + } + } + } + } + repositories { + maven { + url "$System.env.MAVEN_PUBLISH_REGISTRY_URL" + credentials { + username "$System.env.MAVEN_USERNAME" + password "$System.env.MAVEN_PASSWORD" + } + } + } +} + diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/reference.md b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/reference.md new file mode 100644 index 000000000000..c2158eb04344 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/reference.md @@ -0,0 +1,2 @@ +# Reference +## Service diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/build.gradle b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/build.gradle new file mode 100644 index 000000000000..4ee8f227b7af --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() + maven { + url 'https://s01.oss.sonatype.org/content/repositories/releases/' + } +} + +dependencies { + implementation rootProject +} + + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java new file mode 100644 index 000000000000..b99774dbfd47 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/sample-app/src/main/java/sample/App.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +package sample; + +import java.lang.String; + +public final class App { + public static void main(String[] args) { + // import com.seed.javaIdempotencyHeadersFileUpload.AsyncSeedJavaIdempotencyHeadersFileUploadClient + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/settings.gradle b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/settings.gradle new file mode 100644 index 000000000000..9d72108bf70c --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'java-idempotency-headers-file-upload' + +include 'sample-app' \ No newline at end of file diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClient.java new file mode 100644 index 000000000000..d213d276925f --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.Suppliers; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.AsyncServiceClient; +import java.util.function.Supplier; + +public class AsyncSeedJavaIdempotencyHeadersFileUploadClient { + protected final ClientOptions clientOptions; + + protected final Supplier serviceClient; + + public AsyncSeedJavaIdempotencyHeadersFileUploadClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.serviceClient = Suppliers.memoize(() -> new AsyncServiceClient(clientOptions)); + } + + public AsyncServiceClient service() { + return this.serviceClient.get(); + } + + public static AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder builder() { + return new AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder.java new file mode 100644 index 000000000000..1e12d8e0682d --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder.java @@ -0,0 +1,264 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.Environment; +import com.seed.javaIdempotencyHeadersFileUpload.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String token = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + /** + * Sets token + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder token(String token) { + this.token = token; + return this; + } + + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder initialRetryDelayMillis( + long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Sets the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public AsyncSeedJavaIdempotencyHeadersFileUploadClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *

{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.token != null) { + builder.addHeader("Authorization", "Bearer " + this.token); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + if (this.initialRetryDelayMillis.isPresent()) { + builder.initialRetryDelayMillis(this.initialRetryDelayMillis.get()); + } + if (this.maxRetryDelayMillis.isPresent()) { + builder.maxRetryDelayMillis(this.maxRetryDelayMillis.get()); + } + if (this.retryJitterFactor.isPresent()) { + builder.retryJitterFactor(this.retryJitterFactor.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public AsyncSeedJavaIdempotencyHeadersFileUploadClient build() { + if (token == null) { + throw new RuntimeException("Please provide token"); + } + validateConfiguration(); + return new AsyncSeedJavaIdempotencyHeadersFileUploadClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClient.java new file mode 100644 index 000000000000..996fe0fee9c6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClient.java @@ -0,0 +1,28 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.Suppliers; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.ServiceClient; +import java.util.function.Supplier; + +public class SeedJavaIdempotencyHeadersFileUploadClient { + protected final ClientOptions clientOptions; + + protected final Supplier serviceClient; + + public SeedJavaIdempotencyHeadersFileUploadClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.serviceClient = Suppliers.memoize(() -> new ServiceClient(clientOptions)); + } + + public ServiceClient service() { + return this.serviceClient.get(); + } + + public static SeedJavaIdempotencyHeadersFileUploadClientBuilder builder() { + return new SeedJavaIdempotencyHeadersFileUploadClientBuilder(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClientBuilder.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClientBuilder.java new file mode 100644 index 000000000000..0b9eeaf394c9 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/SeedJavaIdempotencyHeadersFileUploadClientBuilder.java @@ -0,0 +1,263 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.Environment; +import com.seed.javaIdempotencyHeadersFileUpload.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import okhttp3.OkHttpClient; + +public class SeedJavaIdempotencyHeadersFileUploadClientBuilder { + private Optional timeout = Optional.empty(); + + private Optional maxRetries = Optional.empty(); + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private final Map customHeaders = new HashMap<>(); + + private String token = null; + + private Environment environment; + + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + + /** + * Sets token + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder token(String token) { + this.token = token; + return this; + } + + public SeedJavaIdempotencyHeadersFileUploadClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder maxRetries(int maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + /** + * Sets the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Sets the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + /** + * Add a custom header to be sent with all requests. + * For headers that need to be computed dynamically or conditionally, use the setAdditional() method override instead. + * + * @param name The header name + * @param value The header value + * @return This builder for method chaining + */ + public SeedJavaIdempotencyHeadersFileUploadClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); + return this; + } + + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(builder); + setAuthentication(builder); + setHttpClient(builder); + setTimeouts(builder); + setRetries(builder); + setLogging(builder); + for (Map.Entry header : this.customHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + setAdditional(builder); + return builder.build(); + } + + /** + * Sets the environment configuration for the client. + * Override this method to modify URLs or add environment-specific logic. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setEnvironment(ClientOptions.Builder builder) { + builder.environment(this.environment); + } + + /** + * Override this method to customize authentication. + * This method is called during client options construction to set up authentication headers. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAuthentication(ClientOptions.Builder builder) {
+     *     super.setAuthentication(builder); // Keep existing auth
+     *     builder.addHeader("X-API-Key", this.apiKey);
+     * }
+     * }
+ */ + protected void setAuthentication(ClientOptions.Builder builder) { + if (this.token != null) { + builder.addHeader("Authorization", "Bearer " + this.token); + } + } + + /** + * Sets the request timeout configuration. + * Override this method to customize timeout behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setTimeouts(ClientOptions.Builder builder) { + if (this.timeout.isPresent()) { + builder.timeout(this.timeout.get()); + } + } + + /** + * Sets the retry configuration for failed requests. + * Override this method to implement custom retry strategies. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setRetries(ClientOptions.Builder builder) { + if (this.maxRetries.isPresent()) { + builder.maxRetries(this.maxRetries.get()); + } + if (this.initialRetryDelayMillis.isPresent()) { + builder.initialRetryDelayMillis(this.initialRetryDelayMillis.get()); + } + if (this.maxRetryDelayMillis.isPresent()) { + builder.maxRetryDelayMillis(this.maxRetryDelayMillis.get()); + } + if (this.retryJitterFactor.isPresent()) { + builder.retryJitterFactor(this.retryJitterFactor.get()); + } + } + + /** + * Sets the OkHttp client configuration. + * Override this method to customize HTTP client behavior (interceptors, connection pools, etc). + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setHttpClient(ClientOptions.Builder builder) { + if (this.httpClient != null) { + builder.httpClient(this.httpClient); + } + } + + /** + * Sets the logging configuration for the SDK. + * Override this method to customize logging behavior. + * + * @param builder The ClientOptions.Builder to configure + */ + protected void setLogging(ClientOptions.Builder builder) { + if (this.logging.isPresent()) { + builder.logging(this.logging.get()); + } + } + + /** + * Override this method to add any additional configuration to the client. + * This method is called at the end of the configuration chain, allowing you to add + * custom headers, modify settings, or perform any other client customization. + * + * @param builder The ClientOptions.Builder to configure + * + * Example: + *
{@code
+     * @Override
+     * protected void setAdditional(ClientOptions.Builder builder) {
+     *     builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+     *     builder.addHeader("X-Client-Version", "1.0.0");
+     * }
+     * }
+ */ + protected void setAdditional(ClientOptions.Builder builder) {} + + /** + * Override this method to add custom validation logic before the client is built. + * This method is called at the beginning of the build() method to ensure the configuration is valid. + * Throw an exception to prevent client creation if validation fails. + * + * Example: + *
{@code
+     * @Override
+     * protected void validateConfiguration() {
+     *     super.validateConfiguration(); // Run parent validations
+     *     if (tenantId == null || tenantId.isEmpty()) {
+     *         throw new IllegalStateException("tenantId is required");
+     *     }
+     * }
+     * }
+ */ + protected void validateConfiguration() {} + + public SeedJavaIdempotencyHeadersFileUploadClient build() { + if (token == null) { + throw new RuntimeException("Please provide token"); + } + validateConfiguration(); + return new SeedJavaIdempotencyHeadersFileUploadClient(buildClientOptions()); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ClientOptions.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ClientOptions.java new file mode 100644 index 000000000000..697e758d4b9c --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ClientOptions.java @@ -0,0 +1,310 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import okhttp3.OkHttpClient; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private final int timeout; + + private final int maxRetries; + + private final Optional initialRetryDelayMillis; + + private final Optional maxRetryDelayMillis; + + private final Optional retryJitterFactor; + + private final Optional logging; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient, + int timeout, + int maxRetries, + Optional initialRetryDelayMillis, + Optional maxRetryDelayMillis, + Optional retryJitterFactor, + Optional logging) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("User-Agent", "com.fern.java-idempotency-headers-file-upload/0.0.1"); + put("X-Fern-Language", "JAVA"); + put("X-Fern-SDK-Name", "com.seed.fern:java-idempotency-headers-file-upload-sdk"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + this.timeout = timeout; + this.maxRetries = maxRetries; + this.initialRetryDelayMillis = initialRetryDelayMillis; + this.maxRetryDelayMillis = maxRetryDelayMillis; + this.retryJitterFactor = retryJitterFactor; + this.logging = logging; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public Map headers(IdempotentRequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public OkHttpClient httpClientWithTimeout(IdempotentRequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int timeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.timeout; + } + return requestOptions.getTimeout().orElse(this.timeout); + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public int maxRetries() { + return this.maxRetries; + } + + public Optional initialRetryDelayMillis() { + return this.initialRetryDelayMillis; + } + + public Optional maxRetryDelayMillis() { + return this.maxRetryDelayMillis; + } + + public Optional retryJitterFactor() { + return this.retryJitterFactor; + } + + public Optional logging() { + return this.logging; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private int maxRetries = 2; + + private Optional initialRetryDelayMillis = Optional.empty(); + + private Optional maxRetryDelayMillis = Optional.empty(); + + private Optional retryJitterFactor = Optional.empty(); + + private Optional timeout = Optional.empty(); + + private OkHttpClient httpClient = null; + + private Optional logging = Optional.empty(); + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(Optional timeout) { + this.timeout = timeout; + return this; + } + + /** + * Override the maximum number of retries. Defaults to 2 retries. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Override the initial delay (in milliseconds) used for exponential backoff between retries. Defaults to 1000 milliseconds. + */ + public Builder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Override the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public Builder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = Optional.of(maxRetryDelayMillis); + return this; + } + + /** + * Override the jitter factor (between 0 and 1) applied to retry delays. Defaults to 0.2. + */ + public Builder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); + return this; + } + + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public Builder logging(LogConfig logging) { + this.logging = Optional.of(logging); + return this; + } + + public ClientOptions build() { + OkHttpClient.Builder httpClientBuilder = + this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); + + if (this.httpClient != null) { + timeout.ifPresent(timeout -> httpClientBuilder + .callTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS)); + } else { + httpClientBuilder + .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .addInterceptor(new RetryInterceptor( + this.maxRetries, + this.initialRetryDelayMillis, + this.maxRetryDelayMillis, + this.retryJitterFactor)); + } + + Logger logger = Logger.from(this.logging); + httpClientBuilder.addInterceptor(new LoggingInterceptor(logger)); + httpClientBuilder.addInterceptor(new ResponseDecompressionInterceptor()); + + this.httpClient = httpClientBuilder.build(); + this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); + + return new ClientOptions( + environment, + headers, + headerSuppliers, + httpClient, + this.timeout.get(), + this.maxRetries, + this.initialRetryDelayMillis, + this.maxRetryDelayMillis, + this.retryJitterFactor, + this.logging); + } + + /** + * Create a new Builder initialized with values from an existing ClientOptions + */ + public static Builder from(ClientOptions clientOptions) { + Builder builder = new Builder(); + builder.environment = clientOptions.environment(); + builder.timeout = Optional.of(clientOptions.timeout(null)); + builder.httpClient = clientOptions.httpClient(); + builder.headers.putAll(clientOptions.headers); + builder.headerSuppliers.putAll(clientOptions.headerSuppliers); + builder.maxRetries = clientOptions.maxRetries(); + builder.initialRetryDelayMillis = clientOptions.initialRetryDelayMillis(); + builder.maxRetryDelayMillis = clientOptions.maxRetryDelayMillis(); + builder.retryJitterFactor = clientOptions.retryJitterFactor(); + builder.logging = clientOptions.logging(); + return builder; + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ConsoleLogger.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ConsoleLogger.java new file mode 100644 index 000000000000..9c95d2b9c471 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ConsoleLogger.java @@ -0,0 +1,51 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.logging.Level; + +/** + * Default logger implementation that writes to the console using {@link java.util.logging.Logger}. + * + *

Uses the "fern" logger name with a simple format of "LEVEL - message". + */ +public final class ConsoleLogger implements ILogger { + + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger("fern"); + + static { + if (logger.getHandlers().length == 0) { + java.util.logging.ConsoleHandler handler = new java.util.logging.ConsoleHandler(); + handler.setFormatter(new java.util.logging.SimpleFormatter() { + @Override + public String format(java.util.logging.LogRecord record) { + return record.getLevel() + " - " + record.getMessage() + System.lineSeparator(); + } + }); + logger.addHandler(handler); + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + } + } + + @Override + public void debug(String message) { + logger.log(Level.FINE, message); + } + + @Override + public void info(String message) { + logger.log(Level.INFO, message); + } + + @Override + public void warn(String message) { + logger.log(Level.WARNING, message); + } + + @Override + public void error(String message) { + logger.log(Level.SEVERE, message); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DateTimeDeserializer.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DateTimeDeserializer.java new file mode 100644 index 000000000000..4e634419608e --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DateTimeDeserializer.java @@ -0,0 +1,70 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting date-time strings into {@link OffsetDateTime} objects. + * Supports ISO 8601 format, space-separated variants, and RFC 1123 (RFC 2822) format. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + String value = parser.getValueAsString(); + TemporalAccessor temporal; + try { + temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest(value, OffsetDateTime::from, LocalDateTime::from); + } catch (DateTimeParseException e) { + try { + // Fall back to space-separated format (e.g. "2025-02-15 10:30:00+00:00"). + temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + value.replace(' ', 'T'), OffsetDateTime::from, LocalDateTime::from); + } catch (DateTimeParseException e2) { + // Fall back to RFC 1123 format (e.g. "Thu, 07 May 2026 14:23:38 +0000"). + temporal = DateTimeFormatter.RFC_1123_DATE_TIME.parseBest( + value, OffsetDateTime::from, LocalDateTime::from); + } + } + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DoubleSerializer.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DoubleSerializer.java new file mode 100644 index 000000000000..73ad8a775756 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/DoubleSerializer.java @@ -0,0 +1,43 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.IOException; + +/** + * Custom serializer that writes integer-valued doubles without a decimal point. + * For example, {@code 24000.0} is serialized as {@code 24000} instead of {@code 24000.0}. + * Non-integer values like {@code 3.14} are serialized normally. + */ +class DoubleSerializer extends JsonSerializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule() + .addSerializer(Double.class, new DoubleSerializer()) + .addSerializer(double.class, new DoubleSerializer()); + } + + /** + * Gets a module wrapping this serializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null && value == Math.floor(value) && !Double.isInfinite(value) && !Double.isNaN(value)) { + gen.writeNumber(value.longValue()); + } else { + gen.writeNumber(value); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Environment.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Environment.java new file mode 100644 index 000000000000..9e9e63309658 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/FileStream.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/FileStream.java new file mode 100644 index 000000000000..35769585998a --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/FileStream.java @@ -0,0 +1,60 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ILogger.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ILogger.java new file mode 100644 index 000000000000..4987aa200d63 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ILogger.java @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +/** + * Interface for custom logger implementations. + * + *

Implement this interface to provide a custom logging backend for the SDK. + * The SDK will call the appropriate method based on the log level. + * + *

Example: + *

{@code
+ * public class MyCustomLogger implements ILogger {
+ *     public void debug(String message) {
+ *         System.out.println("[DBG] " + message);
+ *     }
+ *     public void info(String message) {
+ *         System.out.println("[INF] " + message);
+ *     }
+ *     public void warn(String message) {
+ *         System.out.println("[WRN] " + message);
+ *     }
+ *     public void error(String message) {
+ *         System.out.println("[ERR] " + message);
+ *     }
+ * }
+ * }
+ */ +public interface ILogger { + void debug(String message); + + void info(String message); + + void warn(String message); + + void error(String message); +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotencyUtils.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotencyUtils.java new file mode 100644 index 000000000000..03369e822e79 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotencyUtils.java @@ -0,0 +1,30 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Utilities for auto-generating idempotency keys on mutating requests. + * + *

Only emitted when idempotency-key auto-generation is enabled in the IR + * ({@code SdkConfig.idempotencyKeyGeneration}). + */ +public final class IdempotencyUtils { + + private IdempotencyUtils() {} + + /** + * Returns a copy of {@code headers} with a freshly generated UUIDv4 value added under {@code headerName} when a + * value is not already present. A caller-provided value (whether from a declared idempotency header or the generic + * request-options header bag) always wins. + */ + public static Map withGeneratedIdempotencyKey(Map headers, String headerName) { + Map result = new HashMap<>(headers); + result.putIfAbsent(headerName, UUID.randomUUID().toString()); + return result; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotentRequestOptions.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotentRequestOptions.java new file mode 100644 index 000000000000..00c59b59aa0e --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/IdempotentRequestOptions.java @@ -0,0 +1,184 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class IdempotentRequestOptions { + private final String token; + + private final String idempotencyKey; + + private final String idempotencyExpiration; + + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Optional maxRetries; + + private final Map headers; + + private final Map> headerSuppliers; + + private final Map queryParameters; + + private final Map> queryParameterSuppliers; + + private IdempotentRequestOptions( + String token, + String idempotencyKey, + String idempotencyExpiration, + Optional timeout, + TimeUnit timeoutTimeUnit, + Optional maxRetries, + Map headers, + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { + this.token = token; + this.idempotencyKey = idempotencyKey; + this.idempotencyExpiration = idempotencyExpiration; + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.maxRetries = maxRetries; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Optional getMaxRetries() { + return maxRetries; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + if (this.token != null) { + headers.put("Authorization", "Bearer " + this.token); + } + if (this.idempotencyKey != null) { + headers.put("Idempotency-Key", this.idempotencyKey); + } + if (this.idempotencyExpiration != null) { + headers.put("Idempotency-Expiration", this.idempotencyExpiration); + } + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public Map getQueryParameters() { + Map queryParameters = new HashMap<>(this.queryParameters); + this.queryParameterSuppliers.forEach((key, supplier) -> { + queryParameters.put(key, supplier.get()); + }); + return queryParameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String token = null; + + private String idempotencyKey = null; + + private String idempotencyExpiration = null; + + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private Optional maxRetries = Optional.empty(); + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private final Map queryParameters = new HashMap<>(); + + private final Map> queryParameterSuppliers = new HashMap<>(); + + public Builder token(String token) { + this.token = token; + return this; + } + + public Builder idempotencyKey(String idempotencyKey) { + this.idempotencyKey = idempotencyKey; + return this; + } + + public Builder idempotencyExpiration(String idempotencyExpiration) { + this.idempotencyExpiration = idempotencyExpiration; + return this; + } + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, String value) { + this.queryParameters.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, Supplier value) { + this.queryParameterSuppliers.put(key, value); + return this; + } + + public IdempotentRequestOptions build() { + return new IdempotentRequestOptions( + token, + idempotencyKey, + idempotencyExpiration, + timeout, + timeoutTimeUnit, + maxRetries, + headers, + headerSuppliers, + queryParameters, + queryParameterSuppliers); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/InputStreamRequestBody.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/InputStreamRequestBody.java new file mode 100644 index 000000000000..fe19e8a54555 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/InputStreamRequestBody.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (Source source = Okio.source(inputStream)) { + sink.writeAll(source); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogConfig.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogConfig.java new file mode 100644 index 000000000000..95d2321afcc9 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogConfig.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +/** + * Configuration for SDK logging. + * + *

Use the builder to configure logging behavior: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Or with a custom logger: + *

{@code
+ * LogConfig config = LogConfig.builder()
+ *     .level(LogLevel.DEBUG)
+ *     .logger(new MyCustomLogger())
+ *     .silent(false)
+ *     .build();
+ * }
+ * + *

Defaults: + *

    + *
  • {@code level} — {@link LogLevel#INFO}
  • + *
  • {@code logger} — {@link ConsoleLogger} (writes to stderr via java.util.logging)
  • + *
  • {@code silent} — {@code true} (no output unless explicitly enabled)
  • + *
+ */ +public final class LogConfig { + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + private LogConfig(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + public LogLevel level() { + return level; + } + + public ILogger logger() { + return logger; + } + + public boolean silent() { + return silent; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private LogLevel level = LogLevel.INFO; + private ILogger logger = new ConsoleLogger(); + private boolean silent = true; + + private Builder() {} + + /** + * Set the minimum log level. Only messages at this level or above will be logged. + * Defaults to {@link LogLevel#INFO}. + */ + public Builder level(LogLevel level) { + this.level = level; + return this; + } + + /** + * Set a custom logger implementation. Defaults to {@link ConsoleLogger}. + */ + public Builder logger(ILogger logger) { + this.logger = logger; + return this; + } + + /** + * Set whether logging is silent (disabled). Defaults to {@code true}. + * Set to {@code false} to enable log output. + */ + public Builder silent(boolean silent) { + this.silent = silent; + return this; + } + + public LogConfig build() { + return new LogConfig(level, logger, silent); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogLevel.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogLevel.java new file mode 100644 index 000000000000..f89dd6c0b84f --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LogLevel.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +/** + * Log levels for SDK logging configuration. + * Silent by default — no log output unless explicitly configured. + */ +public enum LogLevel { + DEBUG(1), + INFO(2), + WARN(3), + ERROR(4); + + private final int value; + + LogLevel(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + /** + * Parse a log level from a string (case-insensitive). + * + * @param level the level string (debug, info, warn, error) + * @return the corresponding LogLevel + * @throws IllegalArgumentException if the string does not match any level + */ + public static LogLevel fromString(String level) { + return LogLevel.valueOf(level.toUpperCase()); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Logger.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Logger.java new file mode 100644 index 000000000000..ebbc244e7c50 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Logger.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +/** + * SDK logger that filters messages based on level and silent mode. + * + *

Silent by default — no log output unless explicitly configured. + * Create via {@link LogConfig} or directly: + *

{@code
+ * Logger logger = new Logger(LogLevel.DEBUG, new ConsoleLogger(), false);
+ * logger.debug("request sent");
+ * }
+ */ +public final class Logger { + + private static final Logger DEFAULT = new Logger(LogLevel.INFO, new ConsoleLogger(), true); + + private final LogLevel level; + private final ILogger logger; + private final boolean silent; + + public Logger(LogLevel level, ILogger logger, boolean silent) { + this.level = level; + this.logger = logger; + this.silent = silent; + } + + /** + * Returns a default silent logger (no output). + */ + public static Logger getDefault() { + return DEFAULT; + } + + /** + * Creates a Logger from a {@link LogConfig}. If config is {@code null}, returns the default silent logger. + */ + public static Logger from(LogConfig config) { + if (config == null) { + return DEFAULT; + } + return new Logger(config.level(), config.logger(), config.silent()); + } + + /** + * Creates a Logger from an {@code Optional}. If empty, returns the default silent logger. + */ + public static Logger from(java.util.Optional config) { + return config.map(Logger::from).orElse(DEFAULT); + } + + private boolean shouldLog(LogLevel messageLevel) { + return !silent && level.getValue() <= messageLevel.getValue(); + } + + public boolean isDebug() { + return shouldLog(LogLevel.DEBUG); + } + + public boolean isInfo() { + return shouldLog(LogLevel.INFO); + } + + public boolean isWarn() { + return shouldLog(LogLevel.WARN); + } + + public boolean isError() { + return shouldLog(LogLevel.ERROR); + } + + public void debug(String message) { + if (isDebug()) { + logger.debug(message); + } + } + + public void info(String message) { + if (isInfo()) { + logger.info(message); + } + } + + public void warn(String message) { + if (isWarn()) { + logger.warn(message); + } + } + + public void error(String message) { + if (isError()) { + logger.error(message); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LoggingInterceptor.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LoggingInterceptor.java new file mode 100644 index 000000000000..b1a19805f229 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/LoggingInterceptor.java @@ -0,0 +1,104 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * OkHttp interceptor that logs HTTP requests and responses. + * + *

Logs request method, URL, and headers (with sensitive values redacted) at debug level. + * Logs response status at debug level, and 4xx/5xx responses at error level. + * Does nothing if the logger is silent. + */ +public final class LoggingInterceptor implements Interceptor { + + private static final Set SENSITIVE_HEADERS = new HashSet<>(Arrays.asList( + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "proxy-authenticate", + "proxy-authorization", + "cookie", + "set-cookie", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token")); + + private final Logger logger; + + public LoggingInterceptor(Logger logger) { + this.logger = logger; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Request: ").append(request.method()).append(" ").append(request.url()); + sb.append(" headers={"); + boolean first = true; + for (String name : request.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(request.header(name)); + } + first = false; + } + sb.append("}"); + sb.append(" has_body=").append(request.body() != null); + logger.debug(sb.toString()); + } + + Response response = chain.proceed(request); + + if (logger.isDebug()) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP Response: status=").append(response.code()); + sb.append(" url=").append(response.request().url()); + sb.append(" headers={"); + boolean first = true; + for (String name : response.headers().names()) { + if (!first) { + sb.append(", "); + } + sb.append(name).append("="); + if (SENSITIVE_HEADERS.contains(name.toLowerCase())) { + sb.append("[REDACTED]"); + } else { + sb.append(response.header(name)); + } + first = false; + } + sb.append("}"); + logger.debug(sb.toString()); + } + + if (response.code() >= 400 && logger.isError()) { + logger.error("HTTP Error: status=" + response.code() + " url=" + + response.request().url()); + } + + return response; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/MediaTypes.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/MediaTypes.java new file mode 100644 index 000000000000..be8f20edec55 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Nullable.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Nullable.java new file mode 100644 index 000000000000..d09e2ebeacb6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Nullable.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.Optional; +import java.util.function.Function; + +public final class Nullable { + + private final Either, Null> value; + + private Nullable() { + this.value = Either.left(Optional.empty()); + } + + private Nullable(T value) { + if (value == null) { + this.value = Either.right(Null.INSTANCE); + } else { + this.value = Either.left(Optional.of(value)); + } + } + + public static Nullable ofNull() { + return new Nullable<>(null); + } + + public static Nullable of(T value) { + return new Nullable<>(value); + } + + public static Nullable empty() { + return new Nullable<>(); + } + + public static Nullable ofOptional(Optional value) { + if (value.isPresent()) { + return of(value.get()); + } else { + return empty(); + } + } + + public boolean isNull() { + return this.value.isRight(); + } + + public boolean isEmpty() { + return this.value.isLeft() && !this.value.getLeft().isPresent(); + } + + public T get() { + if (this.isNull()) { + return null; + } + + return this.value.getLeft().get(); + } + + public Nullable map(Function mapper) { + if (this.isNull()) { + return Nullable.ofNull(); + } + + return Nullable.ofOptional(this.value.getLeft().map(mapper)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Nullable)) { + return false; + } + + if (((Nullable) other).isNull() && this.isNull()) { + return true; + } + + return this.value.getLeft().equals(((Nullable) other).value.getLeft()); + } + + private static final class Either { + private L left = null; + private R right = null; + + private Either(L left, R right) { + if (left != null && right != null) { + throw new IllegalArgumentException("Left and right argument cannot both be non-null."); + } + + if (left == null && right == null) { + throw new IllegalArgumentException("Left and right argument cannot both be null."); + } + + if (left != null) { + this.left = left; + } + + if (right != null) { + this.right = right; + } + } + + public static Either left(L left) { + return new Either<>(left, null); + } + + public static Either right(R right) { + return new Either<>(null, right); + } + + public boolean isLeft() { + return this.left != null; + } + + public boolean isRight() { + return this.right != null; + } + + public L getLeft() { + if (!this.isLeft()) { + throw new IllegalArgumentException("Cannot get left from right Either."); + } + return this.left; + } + + public R getRight() { + if (!this.isRight()) { + throw new IllegalArgumentException("Cannot get right from left Either."); + } + return this.right; + } + } + + private static final class Null { + private static final Null INSTANCE = new Null(); + + private Null() {} + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/NullableNonemptyFilter.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/NullableNonemptyFilter.java new file mode 100644 index 000000000000..6f8e686617a4 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/NullableNonemptyFilter.java @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.Optional; + +public final class NullableNonemptyFilter { + @Override + public boolean equals(Object o) { + boolean isOptionalEmpty = isOptionalEmpty(o); + + return isOptionalEmpty; + } + + private boolean isOptionalEmpty(Object o) { + if (o instanceof Optional) { + return !((Optional) o).isPresent(); + } + return false; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ObjectMappers.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ObjectMappers.java new file mode 100644 index 000000000000..fe6ed084e8db --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ObjectMappers.java @@ -0,0 +1,46 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .addModule(DoubleSerializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } + + public static Object parseErrorBody(String responseBodyString) { + try { + return JSON_MAPPER.readValue(responseBodyString, Object.class); + } catch (JsonProcessingException ignored) { + return responseBodyString; + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapper.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapper.java new file mode 100644 index 000000000000..aeaa8d80fed7 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapper.java @@ -0,0 +1,142 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; + +public class QueryStringMapper { + + private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; + + public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + httpUrl.addQueryParameter(key, valueNode.textValue()); + } else { + httpUrl.addQueryParameter(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue()); + } else { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString()); + } + } + } + + public static void addFormDataPart( + MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + multipartBody.addFormDataPart(key, valueNode.textValue()); + } else { + multipartBody.addFormDataPart(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().textValue()); + } else { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().toString()); + } + } + } + + public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator> fields = object.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + + String key = "[" + field.getKey() + "]"; + + if (field.getValue().isObject()) { + List> flatField = + flattenObject((ObjectNode) field.getValue(), arraysAsRepeats); + addAll(flat, flatField, key); + } else if (field.getValue().isArray()) { + List> flatField = + flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats); + addAll(flat, flatField, ""); + } else { + flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue())); + } + } + + return flat; + } + + private static List> flattenArray( + ArrayNode array, String key, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator elements = array.elements(); + + int index = 0; + while (elements.hasNext()) { + JsonNode element = elements.next(); + + String indexKey = key + "[" + index + "]"; + + if (arraysAsRepeats) { + indexKey = key; + } + + if (element.isObject()) { + List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else if (element.isArray()) { + List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else { + flat.add(new AbstractMap.SimpleEntry<>(indexKey, element)); + } + + index++; + } + + return flat; + } + + private static void addAll( + List> target, List> source, String prefix) { + for (Map.Entry entry : source) { + Map.Entry entryToAdd = + new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue()); + target.add(entryToAdd); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RequestOptions.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RequestOptions.java new file mode 100644 index 000000000000..0eb922ba22c6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RequestOptions.java @@ -0,0 +1,154 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class RequestOptions { + private final String token; + + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Optional maxRetries; + + private final Map headers; + + private final Map> headerSuppliers; + + private final Map queryParameters; + + private final Map> queryParameterSuppliers; + + private RequestOptions( + String token, + Optional timeout, + TimeUnit timeoutTimeUnit, + Optional maxRetries, + Map headers, + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { + this.token = token; + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.maxRetries = maxRetries; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Optional getMaxRetries() { + return maxRetries; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + if (this.token != null) { + headers.put("Authorization", "Bearer " + this.token); + } + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public Map getQueryParameters() { + Map queryParameters = new HashMap<>(this.queryParameters); + this.queryParameterSuppliers.forEach((key, supplier) -> { + queryParameters.put(key, supplier.get()); + }); + return queryParameters; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String token = null; + + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private Optional maxRetries = Optional.empty(); + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private final Map queryParameters = new HashMap<>(); + + private final Map> queryParameterSuppliers = new HashMap<>(); + + public Builder token(String token) { + this.token = token; + return this; + } + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = Optional.of(maxRetries); + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, String value) { + this.queryParameters.put(key, value); + return this; + } + + public Builder addQueryParameter(String key, Supplier value) { + this.queryParameterSuppliers.put(key, value); + return this; + } + + public RequestOptions build() { + return new RequestOptions( + token, + timeout, + timeoutTimeUnit, + maxRetries, + headers, + headerSuppliers, + queryParameters, + queryParameterSuppliers); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyInputStream.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyInputStream.java new file mode 100644 index 000000000000..6fcb24b3b313 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyInputStream.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.FilterInputStream; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyReader.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyReader.java new file mode 100644 index 000000000000..72b2b7db3db6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseBodyReader.java @@ -0,0 +1,44 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.FilterReader; +import java.io.IOException; +import okhttp3.Response; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseDecompressionInterceptor.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseDecompressionInterceptor.java new file mode 100644 index 000000000000..a5e9d097371c --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/ResponseDecompressionInterceptor.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.IOException; +import java.util.zip.Inflater; +import okhttp3.Interceptor; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.GzipSource; +import okio.InflaterSource; +import okio.Okio; +import okio.Source; + +/** + * Decompresses gzip and deflate encoded response bodies. OkHttp only performs + * transparent decompression when it adds the Accept-Encoding header itself, so + * responses to requests with an explicitly set Accept-Encoding header would + * otherwise be returned as raw compressed bytes. + */ +public class ResponseDecompressionInterceptor implements Interceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + return decompress(chain.proceed(chain.request())); + } + + private Response decompress(Response response) { + ResponseBody body = response.body(); + if (body == null) { + return response; + } + String encoding = response.header("Content-Encoding"); + if (encoding == null) { + return response; + } + Source decompressedSource; + if (encoding.equalsIgnoreCase("gzip") || encoding.equalsIgnoreCase("x-gzip")) { + decompressedSource = new GzipSource(body.source()); + } else if (encoding.equalsIgnoreCase("deflate")) { + decompressedSource = new InflaterSource(body.source(), new Inflater()); + } else { + return response; + } + ResponseBody decompressedBody = ResponseBody.create(body.contentType(), -1L, Okio.buffer(decompressedSource)); + return response.newBuilder() + .headers(response.headers() + .newBuilder() + .removeAll("Content-Encoding") + .removeAll("Content-Length") + .build()) + .body(decompressedBody) + .build(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RetryInterceptor.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RetryInterceptor.java new file mode 100644 index 000000000000..dcacf006ea9a --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/RetryInterceptor.java @@ -0,0 +1,242 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.Random; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class RetryInterceptor implements Interceptor { + + private static final Duration DEFAULT_INITIAL_RETRY_DELAY = Duration.ofMillis(1000); + private static final Duration DEFAULT_MAX_RETRY_DELAY = Duration.ofMillis(60000); + private static final double DEFAULT_JITTER_FACTOR = 0.2; + + private final int maxRetries; + private final Duration initialRetryDelay; + private final Duration maxRetryDelay; + private final double jitterFactor; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this(maxRetries, Optional.empty(), Optional.empty(), Optional.empty()); + } + + public RetryInterceptor( + int maxRetries, + Optional initialRetryDelayMillis, + Optional maxRetryDelayMillis, + Optional jitterFactor) { + initialRetryDelayMillis.ifPresent(delay -> { + if (delay < 0) { + throw new IllegalArgumentException("initialRetryDelayMillis must be non-negative"); + } + }); + maxRetryDelayMillis.ifPresent(delay -> { + if (delay < 0) { + throw new IllegalArgumentException("maxRetryDelayMillis must be non-negative"); + } + }); + jitterFactor.ifPresent(factor -> { + if (factor < 0 || factor > 1) { + throw new IllegalArgumentException("jitterFactor must be between 0 and 1"); + } + }); + this.maxRetries = maxRetries; + this.initialRetryDelay = initialRetryDelayMillis.map(Duration::ofMillis).orElse(DEFAULT_INITIAL_RETRY_DELAY); + this.maxRetryDelay = maxRetryDelayMillis.map(Duration::ofMillis).orElse(DEFAULT_MAX_RETRY_DELAY); + this.jitterFactor = jitterFactor.orElse(DEFAULT_JITTER_FACTOR); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + int effectiveMaxRetries = resolveMaxRetries(request); + Response response = chain.proceed(request); + + if (shouldRetry(response.code())) { + return retryChain(response, chain, effectiveMaxRetries); + } + + return response; + } + + private int resolveMaxRetries(Request request) { + MaxRetriesOverride override = request.tag(MaxRetriesOverride.class); + return override != null ? override.getValue() : this.maxRetries; + } + + private Response retryChain(Response response, Chain chain, int maxRetries) throws IOException { + ExponentialBackoff backoff = new ExponentialBackoff(maxRetries); + Optional nextBackoff = backoff.nextBackoff(response); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = backoff.nextBackoff(response); + } else { + return response; + } + } + + return response; + } + + /** + * Calculates the retry delay from response headers, with fallback to exponential backoff. + * Priority: Retry-After > X-RateLimit-Reset > Exponential Backoff + */ + private Duration getRetryDelayFromHeaders(Response response, int retryAttempt) { + // Check for Retry-After header first (RFC 7231), with no jitter + String retryAfter = response.header("Retry-After"); + if (retryAfter != null) { + // Parse as number of seconds... + Optional secondsDelay = tryParseLong(retryAfter) + .map(seconds -> seconds * 1000) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(Duration::ofMillis); + if (secondsDelay.isPresent()) { + return secondsDelay.get(); + } + + // ...or as an HTTP date; both are valid + Optional dateDelay = tryParseHttpDate(retryAfter) + .map(resetTime -> resetTime.toInstant().toEpochMilli() - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(Duration::ofMillis); + if (dateDelay.isPresent()) { + return dateDelay.get(); + } + } + + // Then check for industry-standard X-RateLimit-Reset header, with positive jitter + String rateLimitReset = response.header("X-RateLimit-Reset"); + if (rateLimitReset != null) { + // Assume Unix timestamp in epoch seconds + Optional rateLimitDelay = tryParseLong(rateLimitReset) + .map(resetTimeSeconds -> (resetTimeSeconds * 1000) - System.currentTimeMillis()) + .filter(delayMs -> delayMs > 0) + .map(delayMs -> Math.min(delayMs, maxRetryDelay.toMillis())) + .map(this::addPositiveJitter) + .map(Duration::ofMillis); + if (rateLimitDelay.isPresent()) { + return rateLimitDelay.get(); + } + } + + // Fall back to exponential backoff, with symmetric jitter + long initialDelayMillis = initialRetryDelay.toMillis(); + long maxDelayMillis = maxRetryDelay.toMillis(); + long cappedDelay; + if (retryAttempt >= Long.SIZE - 1 || initialDelayMillis > (maxDelayMillis >> retryAttempt)) { + // initialDelayMillis * 2^retryAttempt would exceed maxDelayMillis (or overflow) + cappedDelay = maxDelayMillis; + } else { + cappedDelay = Math.min(initialDelayMillis << retryAttempt, maxDelayMillis); // 2^retryAttempt + } + return Duration.ofMillis(addSymmetricJitter(cappedDelay)); + } + + /** + * Attempts to parse a string as a long, returning empty Optional on failure. + */ + private Optional tryParseLong(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + /** + * Attempts to parse a string as an HTTP date (RFC 1123), returning empty Optional on failure. + */ + private Optional tryParseHttpDate(String value) { + if (value == null) { + return Optional.empty(); + } + try { + return Optional.of(ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + /** + * Adds positive jitter (100-120% of original value) to prevent thundering herd. + * Used for X-RateLimit-Reset header delays. + */ + private long addPositiveJitter(long delayMs) { + double jitterMultiplier = 1.0 + (random.nextDouble() * jitterFactor); + return (long) (delayMs * jitterMultiplier); + } + + /** + * Adds symmetric jitter (90-110% of original value) to prevent thundering herd. + * Used for exponential backoff delays. + */ + private long addSymmetricJitter(long delayMs) { + double jitterMultiplier = 1.0 + ((random.nextDouble() - 0.5) * jitterFactor); + return (long) (delayMs * jitterMultiplier); + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + /** + * Per-request override carried on the OkHttp {@link Request} as a tag. + * When present, the interceptor uses this value instead of the client-wide + * {@code maxRetries} configured at construction time. + */ + public static final class MaxRetriesOverride { + private final int value; + + public MaxRetriesOverride(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff(Response response) { + if (retryNumber >= maxNumRetries) { + return Optional.empty(); + } + + Duration delay = getRetryDelayFromHeaders(response, retryNumber); + retryNumber += 1; + return Optional.of(delay); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Rfc2822DateTimeDeserializer.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Rfc2822DateTimeDeserializer.java new file mode 100644 index 000000000000..6d6735d84ce6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Rfc2822DateTimeDeserializer.java @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Custom deserializer that handles converting RFC 2822 (RFC 1123) dates into {@link OffsetDateTime} objects. + * This is used for fields with format "date-time-rfc-2822", such as Twilio's dateCreated, dateSent, dateUpdated. + */ +public class Rfc2822DateTimeDeserializer extends JsonDeserializer { + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + String raw = parser.getValueAsString(); + return ZonedDateTime.parse(raw, DateTimeFormatter.RFC_1123_DATE_TIME).toOffsetDateTime(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadApiException.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadApiException.java new file mode 100644 index 000000000000..2d2f69df8b7f --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadApiException.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class SeedJavaIdempotencyHeadersFileUploadApiException extends SeedJavaIdempotencyHeadersFileUploadException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + private final Map> headers; + + public SeedJavaIdempotencyHeadersFileUploadApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + } + + public SeedJavaIdempotencyHeadersFileUploadApiException( + String message, int statusCode, Object body, Response rawResponse) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + /** + * @return the headers + */ + public Map> headers() { + return this.headers; + } + + @Override + public String toString() { + return "SeedJavaIdempotencyHeadersFileUploadApiException{" + "message: " + getMessage() + ", statusCode: " + + statusCode + ", body: " + ObjectMappers.stringify(body) + "}"; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadException.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadException.java new file mode 100644 index 000000000000..cc3afc332349 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class SeedJavaIdempotencyHeadersFileUploadException extends RuntimeException { + public SeedJavaIdempotencyHeadersFileUploadException(String message) { + super(message); + } + + public SeedJavaIdempotencyHeadersFileUploadException(String message, Exception e) { + super(message, e); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadHttpResponse.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadHttpResponse.java new file mode 100644 index 000000000000..07ad1d046c31 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SeedJavaIdempotencyHeadersFileUploadHttpResponse.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +public final class SeedJavaIdempotencyHeadersFileUploadHttpResponse { + + private final T body; + + private final Map> headers; + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse(T body, Response rawResponse) { + this.body = body; + + Map> headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + this.headers = headers; + } + + public T body() { + return this.body; + } + + public Map> headers() { + return headers; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEvent.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEvent.java new file mode 100644 index 000000000000..c846802d27c4 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEvent.java @@ -0,0 +1,114 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a Server-Sent Event with all standard fields. + * Used for event-level discrimination where the discriminator is at the SSE envelope level. + * + * @param The type of the data field + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class SseEvent { + private final String event; + private final T data; + private final String id; + private final Long retry; + + private SseEvent(String event, T data, String id, Long retry) { + this.event = event; + this.data = data; + this.id = id; + this.retry = retry; + } + + @JsonProperty("event") + public Optional getEvent() { + return Optional.ofNullable(event); + } + + @JsonProperty("data") + public T getData() { + return data; + } + + @JsonProperty("id") + public Optional getId() { + return Optional.ofNullable(id); + } + + @JsonProperty("retry") + public Optional getRetry() { + return Optional.ofNullable(retry); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SseEvent sseEvent = (SseEvent) o; + return Objects.equals(event, sseEvent.event) + && Objects.equals(data, sseEvent.data) + && Objects.equals(id, sseEvent.id) + && Objects.equals(retry, sseEvent.retry); + } + + @Override + public int hashCode() { + return Objects.hash(event, data, id, retry); + } + + @Override + public String toString() { + return "SseEvent{" + "event='" + + event + '\'' + ", data=" + + data + ", id='" + + id + '\'' + ", retry=" + + retry + '}'; + } + + public static Builder builder() { + return new Builder<>(); + } + + public static final class Builder { + private String event; + private T data; + private String id; + private Long retry; + + private Builder() {} + + public Builder event(String event) { + this.event = event; + return this; + } + + public Builder data(T data) { + this.data = data; + return this; + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder retry(Long retry) { + this.retry = retry; + return this; + } + + public SseEvent build() { + return new SseEvent<>(event, data, id, retry); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEventParser.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEventParser.java new file mode 100644 index 000000000000..319d5de668c0 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/SseEventParser.java @@ -0,0 +1,228 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.type.TypeReference; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Utility class for parsing Server-Sent Events with support for discriminated unions. + *

+ * Handles two discrimination patterns: + *

    + *
  1. Data-level discrimination: The discriminator (e.g., 'type') is inside the JSON data payload. + * Jackson's polymorphic deserialization handles this automatically.
  2. + *
  3. Event-level discrimination: The discriminator (e.g., 'event') is at the SSE envelope level. + * This requires constructing the full SSE envelope for Jackson to process.
  4. + *
+ */ +public final class SseEventParser { + + private static final Set SSE_ENVELOPE_FIELDS = new HashSet<>(Arrays.asList("event", "data", "id", "retry")); + + private SseEventParser() { + // Utility class + } + + /** + * Parse an SSE event using event-level discrimination. + *

+ * Constructs the full SSE envelope object with event, data, id, and retry fields, + * then deserializes it to the target union type. + * + * @param eventType The SSE event type (from event: field) + * @param data The SSE data content (from data: field) + * @param id The SSE event ID (from id: field), may be null + * @param retry The SSE retry value (from retry: field), may be null + * @param unionClass The target union class + * @param discriminatorProperty The property name used for discrimination (e.g., "event") + * @param The target type + * @return The deserialized object + */ + public static T parseEventLevelUnion( + String eventType, String data, String id, Long retry, Class unionClass, String discriminatorProperty) { + try { + // Determine if data should be parsed as JSON based on the variant's expected type + Object parsedData = parseDataForVariant(eventType, data, unionClass, discriminatorProperty); + + // Construct the SSE envelope object + Map envelope = new HashMap<>(); + envelope.put(discriminatorProperty, eventType); + envelope.put("data", parsedData); + if (id != null) { + envelope.put("id", id); + } + if (retry != null) { + envelope.put("retry", retry); + } + + // Serialize to JSON and deserialize to target type + String envelopeJson = ObjectMappers.JSON_MAPPER.writeValueAsString(envelope); + return ObjectMappers.JSON_MAPPER.readValue(envelopeJson, unionClass); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE event with event-level discrimination", e); + } + } + + /** + * Parse an SSE event using data-level discrimination. + *

+ * Simply parses the data field as JSON and deserializes it to the target type. + * Jackson's polymorphic deserialization handles the discrimination automatically. + * + * @param data The SSE data content (from data: field) + * @param valueType The target type + * @param The target type + * @return The deserialized object + */ + public static T parseDataLevelUnion(String data, Class valueType) { + try { + return ObjectMappers.JSON_MAPPER.readValue(data, valueType); + } catch (Exception e) { + throw new RuntimeException("Failed to parse SSE data with data-level discrimination", e); + } + } + + /** + * Determines if the given discriminator property indicates event-level discrimination. + * Event-level discrimination occurs when the discriminator is an SSE envelope field. + * + * @param discriminatorProperty The discriminator property name + * @return true if event-level discrimination, false otherwise + */ + public static boolean isEventLevelDiscrimination(String discriminatorProperty) { + return SSE_ENVELOPE_FIELDS.contains(discriminatorProperty); + } + + /** + * Attempts to find the discriminator property from the union class's Jackson annotations. + * + * @param unionClass The union class to inspect + * @return The discriminator property name, or empty if not found + */ + public static Optional findDiscriminatorProperty(Class unionClass) { + try { + // Look for JsonTypeInfo on the class itself + JsonTypeInfo typeInfo = unionClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + + // Look for inner Value interface with JsonTypeInfo + for (Class innerClass : unionClass.getDeclaredClasses()) { + typeInfo = innerClass.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && !typeInfo.property().isEmpty()) { + return Optional.of(typeInfo.property()); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return Optional.empty(); + } + + /** + * Parse the data field based on what the matching variant expects. + * If the variant expects a String for its data field, returns the raw string. + * Otherwise, parses the data as JSON. + */ + private static Object parseDataForVariant( + String eventType, String data, Class unionClass, String discriminatorProperty) { + if (data == null || data.isEmpty()) { + return data; + } + + try { + // Try to find the variant class that matches this event type + Class variantClass = findVariantClass(unionClass, eventType, discriminatorProperty); + if (variantClass != null) { + // Check if the variant expects a String for the data field + Field dataField = findField(variantClass, "data"); + if (dataField != null && String.class.equals(dataField.getType())) { + // Variant expects String - return raw data + return data; + } + } + + // Try to parse as JSON + return ObjectMappers.JSON_MAPPER.readValue(data, new TypeReference>() {}); + } catch (Exception e) { + // If JSON parsing fails, return as string + return data; + } + } + + /** + * Find the variant class that matches the given discriminator value. + */ + private static Class findVariantClass( + Class unionClass, String discriminatorValue, String discriminatorProperty) { + try { + // Look for JsonSubTypes annotation + JsonSubTypes subTypes = findJsonSubTypes(unionClass); + if (subTypes == null) { + return null; + } + + for (JsonSubTypes.Type subType : subTypes.value()) { + JsonTypeName typeName = subType.value().getAnnotation(JsonTypeName.class); + if (typeName != null && typeName.value().equals(discriminatorValue)) { + return subType.value(); + } + // Also check the name attribute of @JsonSubTypes.Type + if (subType.name().equals(discriminatorValue)) { + return subType.value(); + } + } + } catch (Exception e) { + // Ignore reflection errors + } + return null; + } + + /** + * Find JsonSubTypes annotation on the class or its inner classes. + */ + private static JsonSubTypes findJsonSubTypes(Class unionClass) { + // Check the class itself + JsonSubTypes subTypes = unionClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + + // Check inner classes (for Fern-style unions with inner Value interface) + for (Class innerClass : unionClass.getDeclaredClasses()) { + subTypes = innerClass.getAnnotation(JsonSubTypes.class); + if (subTypes != null) { + return subTypes; + } + } + return null; + } + + /** + * Find a field by name in a class, including private fields. + */ + private static Field findField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // Check superclass + Class superClass = clazz.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return findField(superClass, fieldName); + } + return null; + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Stream.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Stream.java new file mode 100644 index 000000000000..356f52746bfa --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Stream.java @@ -0,0 +1,513 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * Iterable stream for parsing JSON and Server-Sent Events (SSE) data. + * Supports both newline-delimited JSON and SSE with optional stream termination. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable, Closeable { + + private static final String NEWLINE = "\n"; + private static final String DATA_PREFIX = "data:"; + + public enum StreamType { + JSON, + SSE, + SSE_EVENT_DISCRIMINATED + } + + private final Class valueType; + private final Scanner scanner; + private final StreamType streamType; + private final String messageTerminator; + private final String streamTerminator; + private final Reader sseReader; + private final String discriminatorProperty; + private boolean isClosed = false; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.valueType = valueType; + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.streamType = StreamType.JSON; + this.messageTerminator = delimiter; + this.streamTerminator = null; + this.sseReader = null; + this.discriminatorProperty = null; + } + + private Stream(Class valueType, StreamType type, Reader reader, String terminator) { + this(valueType, type, reader, terminator, null); + } + + private Stream( + Class valueType, StreamType type, Reader reader, String terminator, String discriminatorProperty) { + this.valueType = valueType; + this.streamType = type; + this.discriminatorProperty = discriminatorProperty; + if (type == StreamType.JSON) { + this.scanner = new Scanner(reader).useDelimiter(terminator); + this.messageTerminator = terminator; + this.streamTerminator = null; + this.sseReader = null; + } else { + this.scanner = null; + this.messageTerminator = NEWLINE; + this.streamTerminator = terminator; + this.sseReader = reader; + } + } + + public static Stream fromJson(Class valueType, Reader reader, String delimiter) { + return new Stream<>(valueType, reader, delimiter); + } + + public static Stream fromJson(Class valueType, Reader reader) { + return new Stream<>(valueType, reader, NEWLINE); + } + + public static Stream fromSse(Class valueType, Reader sseReader) { + return new Stream<>(valueType, StreamType.SSE, sseReader, null); + } + + public static Stream fromSse(Class valueType, Reader sseReader, String streamTerminator) { + return new Stream<>(valueType, StreamType.SSE, sseReader, streamTerminator); + } + + /** + * Creates a stream from SSE data with event-level discrimination support. + * Use this when the SSE payload is a discriminated union where the discriminator + * is an SSE envelope field (e.g., 'event'). + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty) { + return new Stream<>(valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, null, discriminatorProperty); + } + + /** + * Creates a stream from SSE data with event-level discrimination support and a stream terminator. + * + * @param valueType The class of the objects in the stream. + * @param sseReader The reader that provides the SSE data. + * @param discriminatorProperty The property name used for discrimination (e.g., "event"). + * @param streamTerminator The terminator string that signals end of stream (e.g., "[DONE]"). + * @param The type of objects in the stream. + * @return A new Stream instance configured for SSE with event-level discrimination. + */ + public static Stream fromSseWithEventDiscrimination( + Class valueType, Reader sseReader, String discriminatorProperty, String streamTerminator) { + return new Stream<>( + valueType, StreamType.SSE_EVENT_DISCRIMINATED, sseReader, streamTerminator, discriminatorProperty); + } + + @Override + public void close() throws IOException { + if (!isClosed) { + isClosed = true; + if (scanner != null) { + scanner.close(); + } + if (sseReader != null) { + sseReader.close(); + } + } + } + + private boolean isStreamClosed() { + return isClosed; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + switch (streamType) { + case SSE: + return new SSEIterator(); + case SSE_EVENT_DISCRIMINATED: + return new SSEEventDiscriminatedIterator(); + case JSON: + default: + return new JsonIterator(); + } + } + + private final class JsonIterator implements Iterator { + + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + if (isStreamClosed()) { + return false; + } + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (isStreamClosed()) { + throw new NoSuchElementException("Stream is closed"); + } + + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = + ObjectMappers.JSON_MAPPER.readValue(scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private final class SSEIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + + private SSEIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + // Event ID field (ignored) + } else if (line.startsWith("retry:")) { + // Retry field (ignored) + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + if (eventDataBuffer.length() > 0) { + try { + nextItem = ObjectMappers.JSON_MAPPER.readValue(eventDataBuffer.toString(), valueType); + hasNextItem = true; + eventDataBuffer.setLength(0); + currentEventType = null; + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + eventDataBuffer.setLength(0); + currentEventType = null; + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + } + + /** + * Iterator for SSE streams with event-level discrimination. + * Uses SseEventParser to construct the full SSE envelope for Jackson deserialization. + */ + private final class SSEEventDiscriminatedIterator implements Iterator { + private Scanner sseScanner; + private T nextItem; + private boolean hasNextItem = false; + private boolean endOfStream = false; + private StringBuilder eventDataBuffer = new StringBuilder(); + private String currentEventType = null; + private String currentEventId = null; + private Long currentRetry = null; + + private SSEEventDiscriminatedIterator() { + if (sseReader != null && !isStreamClosed()) { + this.sseScanner = new Scanner(sseReader); + } else { + this.endOfStream = true; + } + } + + @Override + public boolean hasNext() { + if (isStreamClosed() || endOfStream) { + return false; + } + + if (hasNextItem) { + return true; + } + + return readNextMessage(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements in stream"); + } + + T result = nextItem; + nextItem = null; + hasNextItem = false; + return result; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean readNextMessage() { + if (sseScanner == null || isStreamClosed()) { + endOfStream = true; + return false; + } + + try { + while (sseScanner.hasNextLine()) { + String line = sseScanner.nextLine(); + + if (line.trim().isEmpty()) { + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + // Use SseEventParser for event-level discrimination + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse SSE event: " + parseEx.getMessage()); + resetEventState(); + continue; + } + } + continue; + } + + if (line.startsWith(DATA_PREFIX)) { + String dataContent = line.substring(DATA_PREFIX.length()); + if (dataContent.startsWith(" ")) { + dataContent = dataContent.substring(1); + } + + if (eventDataBuffer.length() == 0 + && streamTerminator != null + && dataContent.trim().equals(streamTerminator)) { + endOfStream = true; + return false; + } + + if (eventDataBuffer.length() > 0) { + eventDataBuffer.append('\n'); + } + eventDataBuffer.append(dataContent); + } else if (line.startsWith("event:")) { + String eventValue = line.length() > 6 ? line.substring(6) : ""; + if (eventValue.startsWith(" ")) { + eventValue = eventValue.substring(1); + } + currentEventType = eventValue; + } else if (line.startsWith("id:")) { + String idValue = line.length() > 3 ? line.substring(3) : ""; + if (idValue.startsWith(" ")) { + idValue = idValue.substring(1); + } + currentEventId = idValue; + } else if (line.startsWith("retry:")) { + String retryValue = line.length() > 6 ? line.substring(6) : ""; + if (retryValue.startsWith(" ")) { + retryValue = retryValue.substring(1); + } + try { + currentRetry = Long.parseLong(retryValue.trim()); + } catch (NumberFormatException e) { + // Ignore invalid retry values + } + } else if (line.startsWith(":")) { + // Comment line (ignored) + } + } + + // Handle any remaining buffered data at end of stream + if (eventDataBuffer.length() > 0 || currentEventType != null) { + try { + nextItem = SseEventParser.parseEventLevelUnion( + currentEventType, + eventDataBuffer.toString(), + currentEventId, + currentRetry, + valueType, + discriminatorProperty); + hasNextItem = true; + resetEventState(); + return true; + } catch (Exception parseEx) { + System.err.println("Failed to parse final SSE event: " + parseEx.getMessage()); + resetEventState(); + } + } + + endOfStream = true; + return false; + + } catch (Exception e) { + System.err.println("Failed to parse SSE stream: " + e.getMessage()); + endOfStream = true; + return false; + } + } + + private void resetEventState() { + eventDataBuffer.setLength(0); + currentEventType = null; + currentEventId = null; + currentRetry = null; + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Suppliers.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Suppliers.java new file mode 100644 index 000000000000..76048cd2c13a --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncRawServiceClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncRawServiceClient.java new file mode 100644 index 000000000000..98bd1c7faac6 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncRawServiceClient.java @@ -0,0 +1,353 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.resources.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.FileStream; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotencyUtils; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotentRequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.ObjectMappers; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.RetryInterceptor; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadApiException; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadException; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadHttpResponse; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.requests.UploadRequest; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawServiceClient { + protected final ClientOptions clientOptions; + + public AsyncRawServiceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public CompletableFuture> upload( + File file, UploadRequest request) { + return upload(file, request, null); + } + + public CompletableFuture> upload( + File file, UploadRequest request, IdempotentRequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + try { + String fileMimeType = Files.probeContentType(file.toPath()); + MediaType fileMimeTypeMediaType = fileMimeType != null ? MediaType.parse(fileMimeType) : null; + multipartBodyBuilder.addFormDataPart( + "file", file.getName(), RequestBody.create(file, fileMimeTypeMediaType)); + if (request.getDescription().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "description", request.getDescription().get()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("POST", multipartBodyBuilder.build()) + .headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + clientOptions.headers(requestOptions), "Idempotency-Key"))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> upload( + InputStream stream, String filename) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers((RequestOptions) null), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> upload( + InputStream stream, String filename, MediaType mediaType) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers((RequestOptions) null), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> upload( + InputStream stream, String filename, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers(requestOptions), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> upload( + InputStream stream, String filename, MediaType mediaType, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers(requestOptions), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response)); + return; + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new SeedJavaIdempotencyHeadersFileUploadException( + "Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally( + new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncServiceClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncServiceClient.java new file mode 100644 index 000000000000..6c2dd859b8a3 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/AsyncServiceClient.java @@ -0,0 +1,58 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.resources.service; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotentRequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.requests.UploadRequest; +import java.io.File; +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; +import okhttp3.MediaType; + +public class AsyncServiceClient { + protected final ClientOptions clientOptions; + + private final AsyncRawServiceClient rawClient; + + public AsyncServiceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawServiceClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawServiceClient withRawResponse() { + return this.rawClient; + } + + public CompletableFuture upload(File file, UploadRequest request) { + return this.rawClient.upload(file, request).thenApply(response -> response.body()); + } + + public CompletableFuture upload(File file, UploadRequest request, IdempotentRequestOptions requestOptions) { + return this.rawClient.upload(file, request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture upload(InputStream stream, String filename) { + return this.rawClient.upload(stream, filename).thenApply(response -> response.body()); + } + + public CompletableFuture upload(InputStream stream, String filename, MediaType mediaType) { + return this.rawClient.upload(stream, filename, mediaType).thenApply(response -> response.body()); + } + + public CompletableFuture upload(InputStream stream, String filename, RequestOptions requestOptions) { + return this.rawClient.upload(stream, filename, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture upload( + InputStream stream, String filename, MediaType mediaType, RequestOptions requestOptions) { + return this.rawClient + .upload(stream, filename, mediaType, requestOptions) + .thenApply(response -> response.body()); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/RawServiceClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/RawServiceClient.java new file mode 100644 index 000000000000..1af3c440b880 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/RawServiceClient.java @@ -0,0 +1,272 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.resources.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.FileStream; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotencyUtils; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotentRequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.ObjectMappers; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.RetryInterceptor; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadApiException; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadException; +import com.seed.javaIdempotencyHeadersFileUpload.core.SeedJavaIdempotencyHeadersFileUploadHttpResponse; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.requests.UploadRequest; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawServiceClient { + protected final ClientOptions clientOptions; + + public RawServiceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload(File file, UploadRequest request) { + return upload(file, request, null); + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload( + File file, UploadRequest request, IdempotentRequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + try { + String fileMimeType = Files.probeContentType(file.toPath()); + MediaType fileMimeTypeMediaType = fileMimeType != null ? MediaType.parse(fileMimeType) : null; + multipartBodyBuilder.addFormDataPart( + "file", file.getName(), RequestBody.create(file, fileMimeTypeMediaType)); + if (request.getDescription().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "description", request.getDescription().get()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("POST", multipartBodyBuilder.build()) + .headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + clientOptions.headers(requestOptions), "Idempotency-Key"))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e); + } + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload(InputStream stream, String filename) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers((RequestOptions) null), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e); + } + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload( + InputStream stream, String filename, MediaType mediaType) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers((RequestOptions) null), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e); + } + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload( + InputStream stream, String filename, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers(requestOptions), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e); + } + } + + public SeedJavaIdempotencyHeadersFileUploadHttpResponse upload( + InputStream stream, String filename, MediaType mediaType, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("documents"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(IdempotencyUtils.withGeneratedIdempotencyKey( + this.clientOptions.headers(requestOptions), "Idempotency-Key"))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new SeedJavaIdempotencyHeadersFileUploadHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, String.class), response); + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new SeedJavaIdempotencyHeadersFileUploadApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException( + "Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new SeedJavaIdempotencyHeadersFileUploadException("Network error executing HTTP request", e); + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/ServiceClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/ServiceClient.java new file mode 100644 index 000000000000..18dd72b8e952 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/ServiceClient.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.resources.service; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ClientOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.IdempotentRequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.core.RequestOptions; +import com.seed.javaIdempotencyHeadersFileUpload.resources.service.requests.UploadRequest; +import java.io.File; +import java.io.InputStream; +import okhttp3.MediaType; + +public class ServiceClient { + protected final ClientOptions clientOptions; + + private final RawServiceClient rawClient; + + public ServiceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawServiceClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawServiceClient withRawResponse() { + return this.rawClient; + } + + public String upload(File file, UploadRequest request) { + return this.rawClient.upload(file, request).body(); + } + + public String upload(File file, UploadRequest request, IdempotentRequestOptions requestOptions) { + return this.rawClient.upload(file, request, requestOptions).body(); + } + + public String upload(InputStream stream, String filename) { + return this.rawClient.upload(stream, filename).body(); + } + + public String upload(InputStream stream, String filename, MediaType mediaType) { + return this.rawClient.upload(stream, filename, mediaType).body(); + } + + public String upload(InputStream stream, String filename, RequestOptions requestOptions) { + return this.rawClient.upload(stream, filename, requestOptions).body(); + } + + public String upload(InputStream stream, String filename, MediaType mediaType, RequestOptions requestOptions) { + return this.rawClient + .upload(stream, filename, mediaType, requestOptions) + .body(); + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/requests/UploadRequest.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/requests/UploadRequest.java new file mode 100644 index 000000000000..99ed4f4edd17 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/main/java/com/seed/javaIdempotencyHeadersFileUpload/resources/service/requests/UploadRequest.java @@ -0,0 +1,105 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.resources.service.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.seed.javaIdempotencyHeadersFileUpload.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = UploadRequest.Builder.class) +public final class UploadRequest { + private final Optional description; + + private final Map additionalProperties; + + private UploadRequest(Optional description, Map additionalProperties) { + this.description = description; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("description") + public Optional getDescription() { + return description; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof UploadRequest && equalTo((UploadRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(UploadRequest other) { + return description.equals(other.description); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.description); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional description = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(UploadRequest other) { + description(other.getDescription()); + return this; + } + + @JsonSetter(value = "description", nulls = Nulls.SKIP) + public Builder description(Optional description) { + this.description = description; + return this; + } + + public Builder description(String description) { + this.description = Optional.ofNullable(description); + return this; + } + + public UploadRequest build() { + return new UploadRequest(description, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/StreamTest.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/StreamTest.java new file mode 100644 index 000000000000..f6be512a87fc --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/StreamTest.java @@ -0,0 +1,120 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +import static org.junit.jupiter.api.Assertions.*; + +import com.seed.javaIdempotencyHeadersFileUpload.core.ObjectMappers; +import com.seed.javaIdempotencyHeadersFileUpload.core.Stream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +public final class StreamTest { + @Test + public void testJsonStream() { + List> messages = + Arrays.asList(createMap("message", "hello"), createMap("message", "world")); + List jsonStrings = messages.stream().map(StreamTest::mapToJson).collect(Collectors.toList()); + String input = String.join("\n", jsonStrings); + StringReader jsonInput = new StringReader(input); + Stream jsonStream = Stream.fromJson(Map.class, jsonInput); + int expectedMessages = 2; + int actualMessages = 0; + for (Map jsonObject : jsonStream) { + actualMessages++; + assertTrue(jsonObject.containsKey("message")); + } + assertEquals(expectedMessages, actualMessages); + } + + @Test + public void testSseStream() { + List> events = Arrays.asList(createMap("event", "start"), createMap("event", "end")); + List sseStrings = events.stream().map(StreamTest::mapToSse).collect(Collectors.toList()); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("event")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseStreamWithTerminator() { + List> events = Arrays.asList(createMap("message", "first"), createMap("message", "second")); + List sseStrings = + new ArrayList<>(events.stream().map(StreamTest::mapToSse).collect(Collectors.toList())); + sseStrings.add("data: [DONE]"); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSse(Map.class, sseInput, "[DONE]"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + assertTrue(eventData.containsKey("message")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testSseEventDiscriminatedStream() { + List sseStrings = Arrays.asList( + mapToSseWithEvent("start", createMap("status", "pending")), + mapToSseWithEvent("end", createMap("status", "complete"))); + String input = String.join("\n" + "\n", sseStrings); + StringReader sseInput = new StringReader(input); + Stream sseStream = Stream.fromSseWithEventDiscrimination(Map.class, sseInput, "event"); + int expectedEvents = 2; + int actualEvents = 0; + for (Map eventData : sseStream) { + actualEvents++; + // Event-level discrimination includes the event field in the parsed result + assertTrue(eventData.containsKey("event")); + assertTrue(eventData.containsKey("data")); + } + assertEquals(expectedEvents, actualEvents); + } + + @Test + public void testStreamResourceManagement() throws IOException { + StringReader testInput = new StringReader("{\"test\":\"data\"}"); + Stream testStream = Stream.fromJson(Map.class, testInput); + testStream.close(); + assertFalse(testStream.iterator().hasNext()); + } + + private static String mapToJson(Map map) { + try { + return ObjectMappers.JSON_MAPPER.writeValueAsString(map); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static String mapToSse(Map map) { + return "data: " + mapToJson(map); + } + + private static String mapToSseWithEvent(String eventType, Map data) { + return "event: " + eventType + "\n" + "data: " + mapToJson(data); + } + + private static Map createMap(String key, String value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/TestClient.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/TestClient.java new file mode 100644 index 000000000000..bd2ac145d367 --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/TestClient.java @@ -0,0 +1,11 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload; + +public final class TestClient { + public void test() { + // Add tests here and mark this file in .fernignore + assert true; + } +} diff --git a/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapperTest.java b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapperTest.java new file mode 100644 index 000000000000..ce153d6f509d --- /dev/null +++ b/seed/java-sdk/java-idempotency-headers-file-upload/auto-generate-idempotency-key/src/test/java/com/seed/javaIdempotencyHeadersFileUpload/core/QueryStringMapperTest.java @@ -0,0 +1,339 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.seed.javaIdempotencyHeadersFileUpload.core; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.HttpUrl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class QueryStringMapperTest { + @Test + public void testObjectWithQuotedString_indexedArrays() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithQuotedString_arraysAsRepeats() { + Map map = new HashMap() { + { + put("hello", "\"world\""); + } + }; + + String expectedQueryString = "withquoted%5Bhello%5D=%22world%22"; + + String actualQueryString = queryString( + new HashMap() { + { + put("withquoted", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_indexedArrays() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObject_arraysAsRepeats() { + Map map = new HashMap() { + { + put("foo", "bar"); + put("baz", "qux"); + } + }; + + String expectedQueryString = "metadata%5Bfoo%5D=bar&metadata%5Bbaz%5D=qux"; + + String actualQueryString = queryString( + new HashMap() { + { + put("metadata", map); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_indexedArrays() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testNestedObject_arraysAsRepeats() { + Map> nestedMap = new HashMap>() { + { + put("mapkey1", new HashMap() { + { + put("mapkey1mapkey1", "mapkey1mapkey1value"); + put("mapkey1mapkey2", "mapkey1mapkey2value"); + } + }); + put("mapkey2", new HashMap() { + { + put("mapkey2mapkey1", "mapkey2mapkey1value"); + } + }); + } + }; + + String expectedQueryString = + "nested%5Bmapkey2%5D%5Bmapkey2mapkey1%5D=mapkey2mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey1" + + "%5D=mapkey1mapkey1value&nested%5Bmapkey1%5D%5Bmapkey1mapkey2%5D=mapkey1mapkey2value"; + + String actualQueryString = queryString( + new HashMap() { + { + put("nested", nestedMap); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_indexedArrays() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testDateTime_arraysAsRepeats() { + OffsetDateTime dateTime = + OffsetDateTime.ofInstant(Instant.ofEpochSecond(1740412107L), ZoneId.of("America/New_York")); + + String expectedQueryString = "datetime=2025-02-24T10%3A48%3A27-05%3A00"; + + String actualQueryString = queryString( + new HashMap() { + { + put("datetime", dateTime); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_indexedArrays() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = "objects%5B0%5D%5Bvalue%5D=world&objects%5B0%5D%5Bkey%5D=hello&objects%5B1%5D" + + "%5Bvalue%5D=bar&objects%5B1%5D%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectArray_arraysAsRepeats() { + List> mapArray = new ArrayList>() { + { + add(new HashMap() { + { + put("key", "hello"); + put("value", "world"); + } + }); + add(new HashMap() { + { + put("key", "foo"); + put("value", "bar"); + } + }); + add(new HashMap<>()); + } + }; + + String expectedQueryString = + "objects%5Bvalue%5D=world&objects%5Bkey%5D=hello&objects%5Bvalue" + "%5D=bar&objects%5Bkey%5D=foo"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objects", mapArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_indexedArrays() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = + "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds%5D%5B0%5D=id1&objectwitharray" + + "%5BcontactIds%5D%5B1%5D=id2&objectwitharray%5BcontactIds%5D%5B2%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + false); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + @Test + public void testObjectWithArray_arraysAsRepeats() { + Map objectWithArray = new HashMap() { + { + put("id", "abc123"); + put("contactIds", new ArrayList() { + { + add("id1"); + add("id2"); + add("id3"); + } + }); + } + }; + + String expectedQueryString = "objectwitharray%5Bid%5D=abc123&objectwitharray%5BcontactIds" + + "%5D=id1&objectwitharray%5BcontactIds%5D=id2&objectwitharray%5BcontactIds%5D=id3"; + + String actualQueryString = queryString( + new HashMap() { + { + put("objectwitharray", objectWithArray); + } + }, + true); + + Assertions.assertEquals(expectedQueryString, actualQueryString); + } + + private static String queryString(Map params, boolean arraysAsRepeats) { + HttpUrl.Builder httpUrl = HttpUrl.parse("http://www.fakewebsite.com/").newBuilder(); + params.forEach((paramName, paramValue) -> + QueryStringMapper.addQueryParameter(httpUrl, paramName, paramValue, arraysAsRepeats)); + return httpUrl.build().encodedQuery(); + } +} diff --git a/seed/java-sdk/seed.yml b/seed/java-sdk/seed.yml index a1c98e5fa0cf..2b95dd5a59cf 100644 --- a/seed/java-sdk/seed.yml +++ b/seed/java-sdk/seed.yml @@ -140,8 +140,17 @@ fixtures: license: custom: LICENSE outputFolder: custom-license + idempotency-headers: + - outputFolder: no-custom-config + customConfig: null + - outputFolder: auto-generate-idempotency-key + customConfig: + auto-generate-idempotency-key: true java-idempotency-headers-file-upload: - outputFolder: default + - outputFolder: auto-generate-idempotency-key + customConfig: + auto-generate-idempotency-key: true java-inline-types: - customConfig: enable-inline-types: true From f6dab18986bb66a746fa1f7ee7caf7110f0cc375 Mon Sep 17 00:00:00 2001 From: cade Date: Sat, 11 Jul 2026 00:05:58 +0000 Subject: [PATCH 2/2] chore(java): chain IOException cause in IdempotencyUtilsGenerator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../fern/java/client/generators/IdempotencyUtilsGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java b/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java index 613bb33a561e..f94b764d1b8d 100644 --- a/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java +++ b/generators/java/sdk/src/main/java/com/fern/java/client/generators/IdempotencyUtilsGenerator.java @@ -38,7 +38,7 @@ public GeneratedResourcesJavaFile generateFile() { .contents(contents) .build(); } catch (IOException e) { - throw new RuntimeException("Failed to read IdempotencyUtils.java"); + throw new RuntimeException("Failed to read IdempotencyUtils.java", e); } } }