From ce79ce11a1f46f69bc78a05b93bdcff92c1dec61 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 20 Jul 2026 12:31:30 +0530 Subject: [PATCH 1/6] SK-2955 align upsert/upsertType with Flow-DB changes (docs, tests, error-message cleanup) (#346) Flow-DB now defaults record-level upsert to UPDATE (was REPLACE) and owns upsert-placement validation server-side. The SDK already serializes upsertType correctly and omits updateType when unset, so this is docs, tests, and validation hygiene only. - README: document upsertType is optional, default is now UPDATE, and UPDATE vs REPLACE semantics. - Samples: drop redundant explicit upsertType(UPDATE) (now the default) in BulkMultiTableInsert{Sync,Async}; keep REPLACE samples as-is. - Validations: defer upsert-placement checks to the backend (authoritative messages); SDK only guards empty upsert columns. - Tests: cover default-UPDATE omission at request/record level, both-level serialization, and updated placement tests to reflect deferral. Co-authored-by: Claude Opus 4.8 (1M context) --- README.md | 7 +- samples/pom.xml | 2 +- .../vault/BulkMultiTableInsertAsync.java | 4 +- .../vault/BulkMultiTableInsertSync.java | 4 +- .../utils/validations/Validations.java | 29 ++------ .../java/com/skyflow/VaultClientTests.java | 69 +++++++++++++++++++ .../utils/validations/ValidationsTests.java | 12 ++-- .../com/skyflow/vault/data/InsertTests.java | 31 ++------- 8 files changed, 99 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 938c7b89..fc4354d2 100644 --- a/README.md +++ b/README.md @@ -260,8 +260,11 @@ public class InsertSchema { **Note**: - The table name can be specified either at the request level `InsertRequest` or at the record level `InsertRecord`, but not both. - If table name is not specified at the request level `InsertRequest`, then it must be specified in all record objects. -- If table name is specified at the request level `InsertRequest`, then upsert must also be specified at the request level. -- If table name is specified at the record level `InsertRecord`, then upsert must also be specified at the record level `InsertRecord`. +- Upsert must be specified in the same place as the table name: if table name is specified at the request level `InsertRequest`, specify upsert at the request level; if table name is specified at the record level `InsertRecord`, specify upsert at the record level `InsertRecord`. +- `upsertType` is optional and can be set alongside `upsert` at either the request level or the record level (matching the table/upsert placement): + - `UpsertType.UPDATE` — updates only the columns provided in the request on the matched row; other existing columns are retained. + - `UpsertType.REPLACE` — replaces the matched row with the provided values; columns not provided are cleared. + - If `upsertType` is not specified, the vault applies the default of `UPDATE`. ### An [example](https://github.com/skyflowapi/skyflow-java/blob/v3/samples/src/main/java/com/example/vault/BulkInsertSync.java) of a sync bulkInsert call diff --git a/samples/pom.xml b/samples/pom.xml index ad4a1427..4675a641 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -18,7 +18,7 @@ com.skyflow skyflow-java - 3.0.0-beta.6 + 3.0.0-beta.11 diff --git a/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java b/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java index 1606cf70..99c5e99a 100644 --- a/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java +++ b/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -5,7 +5,6 @@ import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; -import com.skyflow.enums.UpsertType; import com.skyflow.vault.data.InsertRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertResponse; @@ -59,7 +58,8 @@ public static void main(String[] args) { .data(recordData1) .table("") .upsert(upsertColumns) - .upsertType(UpsertType.UPDATE) + // upsertType is optional; when omitted the vault defaults to UpsertType.UPDATE. + // Set .upsertType(UpsertType.REPLACE) to replace the matched row instead. .build(); // Step 5: Prepare second record for insertion diff --git a/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java index 95bf50dc..577cd142 100644 --- a/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java +++ b/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -5,7 +5,6 @@ import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; -import com.skyflow.enums.UpsertType; import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.InsertRecord; import com.skyflow.vault.data.InsertRequest; @@ -58,7 +57,8 @@ public static void main(String[] args) { .data(recordData1) .table("") .upsert(upsertColumns) - .upsertType(UpsertType.UPDATE) + // upsertType is optional; when omitted the vault defaults to UpsertType.UPDATE. + // Set .upsertType(UpsertType.REPLACE) to replace the matched row instead. .build(); // Step 5: Prepare second record for insertion diff --git a/v3/src/main/java/com/skyflow/utils/validations/Validations.java b/v3/src/main/java/com/skyflow/utils/validations/Validations.java index 839c54ce..3e30f7a9 100644 --- a/v3/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/v3/src/main/java/com/skyflow/utils/validations/Validations.java @@ -73,30 +73,15 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky } } } - // upsert check 1 - if (insertRequest.getTable() != null && !table.trim().isEmpty()){ // if table name specified at both place - for (InsertRecord record : records) { - if (record.getUpsert() != null && record.getUpsert().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); - } - if (record.getUpsert() != null && !record.getUpsert().isEmpty()){ - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); - } - } - } - // upsert check 2 - if (insertRequest.getTable() == null || table.trim().isEmpty()){ - if (insertRequest.getUpsert() != null && !insertRequest.getUpsert().isEmpty()){ + // Upsert placement (request level vs record level) is validated by the backend, + // which returns the authoritative error message. The SDK only guards against + // empty upsert column lists. + for (InsertRecord record : records) { + if (record.getUpsert() != null && record.getUpsert().isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL.getLog(), InterfaceName.INSERT.getName() + ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); } } diff --git a/v3/src/test/java/com/skyflow/VaultClientTests.java b/v3/src/test/java/com/skyflow/VaultClientTests.java index b52272dc..e9b5a65f 100644 --- a/v3/src/test/java/com/skyflow/VaultClientTests.java +++ b/v3/src/test/java/com/skyflow/VaultClientTests.java @@ -279,6 +279,75 @@ public void testMixedTableAndUpsertLevels() { Assert.assertEquals("col4", result.getUpsert().get().getUniqueColumns().get().get(0)); } + @Test + public void testUpsertAtRequestLevelWithoutUpsertTypeOmitsUpdateType() { + // When upsertType is not specified, the SDK must not send updateType so the + // backend applies its default (UPDATE). + Map data = new HashMap<>(); + data.put("key", "value"); + InsertRecord record = InsertRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + + com.skyflow.vault.data.InsertRequest request = + com.skyflow.vault.data.InsertRequest.builder() + .records(records) + .upsert(Arrays.asList("col1")) + .build(); + + V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); + Assert.assertNotNull(result.getUpsert()); + Assert.assertEquals("col1", result.getUpsert().get().getUniqueColumns().get().get(0)); + Assert.assertFalse(result.getUpsert().get().getUpdateType().isPresent()); + } + + @Test + public void testUpsertAtRecordLevelWithoutUpsertTypeOmitsUpdateType() { + // When upsertType is not specified at the record level, the SDK must not send + // updateType so the backend applies its default (UPDATE). + Map data = new HashMap<>(); + data.put("key", "value"); + InsertRecord record = InsertRecord.builder().data(data).upsert(Arrays.asList("col2")).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + + com.skyflow.vault.data.InsertRequest request = + com.skyflow.vault.data.InsertRequest.builder() + .records(records) + .build(); + + V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); + Assert.assertNotNull(result.getRecords().get().get(0).getUpsert()); + Assert.assertEquals("col2", result.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get().get(0)); + Assert.assertFalse(result.getRecords().get().get(0).getUpsert().get().getUpdateType().isPresent()); + } + + @Test + public void testUpsertTypeAtBothRequestAndRecordLevelSerializesBoth() { + // The SDK serializes updateType wherever it is set; the backend rejects the + // both-levels combination. This asserts the SDK does not silently drop either. + Map data = new HashMap<>(); + data.put("key", "value"); + InsertRecord record = InsertRecord.builder() + .data(data) + .upsert(Arrays.asList("col2")) + .upsertType(UpsertType.UPDATE) + .build(); + ArrayList records = new ArrayList<>(); + records.add(record); + + com.skyflow.vault.data.InsertRequest request = + com.skyflow.vault.data.InsertRequest.builder() + .records(records) + .upsert(Arrays.asList("col1")) + .upsertType(UpsertType.REPLACE) + .build(); + + V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); + Assert.assertEquals("REPLACE", result.getUpsert().get().getUpdateType().get().name()); + Assert.assertEquals("UPDATE", result.getRecords().get().get(0).getUpsert().get().getUpdateType().get().name()); + } + @Test public void testSetBearerTokenWhenTokenIsNull() throws Exception { Credentials credentials = new Credentials(); diff --git a/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java b/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java index 407ea055..cd4d7485 100644 --- a/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/v3/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -75,7 +75,8 @@ public void validateInsertRequest_tableMissing_throws() { } @Test - public void validateInsertRequest_upsertAtRecordLevel_throws() { + public void validateInsertRequest_upsertAtRecordLevelWithTableAtRequestLevel_deferredToBackend() throws SkyflowException { + // Upsert placement is no longer validated client-side; the backend owns the rule. ArrayList records = new ArrayList<>(); records.add(InsertRecord.builder() .table(null) @@ -85,12 +86,12 @@ public void validateInsertRequest_upsertAtRecordLevel_throws() { .table("requestTable") .records(records) .build(); - assertSkyflowException(() -> Validations.validateInsertRequest(request), - ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); + Validations.validateInsertRequest(request); } @Test - public void validateInsertRequest_upsertAtRequestLevelWithoutTable_throws() { + public void validateInsertRequest_upsertAtRequestLevelWithTableAtRecordLevel_deferredToBackend() throws SkyflowException { + // Upsert placement is no longer validated client-side; the backend owns the rule. ArrayList records = new ArrayList<>(); records.add(InsertRecord.builder().table("recordTable").build()); InsertRequest request = InsertRequest.builder() @@ -98,8 +99,7 @@ public void validateInsertRequest_upsertAtRequestLevelWithoutTable_throws() { .upsert(Collections.singletonList("key")) .records(records) .build(); - assertSkyflowException(() -> Validations.validateInsertRequest(request), - ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); + Validations.validateInsertRequest(request); } @Test diff --git a/v3/src/test/java/com/skyflow/vault/data/InsertTests.java b/v3/src/test/java/com/skyflow/vault/data/InsertTests.java index 7020fb19..2b12b73f 100644 --- a/v3/src/test/java/com/skyflow/vault/data/InsertTests.java +++ b/v3/src/test/java/com/skyflow/vault/data/InsertTests.java @@ -326,6 +326,9 @@ public void testTableSpecifiedAtBothRequestAndRecordLevel() { } } + // Upsert placement (request vs record level) is validated by the backend, not the SDK. + // The SDK must accept these combinations and let the backend return the authoritative error. + @Test public void testUpsertSpecifiedAtBothRequestAndRecordLevel() { InsertRecord record = InsertRecord.builder().data(valueMap).upsert(upsert).build(); @@ -333,13 +336,8 @@ public void testUpsertSpecifiedAtBothRequestAndRecordLevel() { InsertRequest request = InsertRequest.builder().table(table).records(values).upsert(upsert).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @@ -350,13 +348,8 @@ public void testUpsertAtRecordLevelWithTableAtRequestLevel() { InsertRequest request = InsertRequest.builder().table(table).records(values).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @@ -367,13 +360,8 @@ public void testUpsertAtRequestLevelWithNoTable() { InsertRequest request = InsertRequest.builder().records(values).upsert(upsert).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail(INVALID_EXCEPTION_THROWN); } } @@ -387,13 +375,8 @@ public void testUpsertAtRequestLevelWithEmptyTable() { InsertRequest request = InsertRequest.builder().table("").records(values).upsert(upsert).build(); try { Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); + Assert.fail(INVALID_EXCEPTION_THROWN); } } From 1cf97434a03df93ec9ff341a02513846d6b4974a Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 20 Jul 2026 07:23:05 +0000 Subject: [PATCH 2/6] [AUTOMATED] Private Release 2.1.1-dev-ce79ce1 --- v3/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/pom.xml b/v3/pom.xml index 8d0f10de..d27fa575 100644 --- a/v3/pom.xml +++ b/v3/pom.xml @@ -11,7 +11,7 @@ skyflow-java - 3.0.0-beta.11 + 2.1.1-dev.ce79ce1 jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language From 1236334ded3261e6dd1bf46c24017817b53f419f Mon Sep 17 00:00:00 2001 From: skyflow-bharti <118584001+skyflow-bharti@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:13:10 +0530 Subject: [PATCH 3/6] SK-3002 retry backoff support (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SK-3002 add SkyflowRetryInterceptor (configurable retry count + backoff) Hand-written OkHttp interceptor replicating Fern's retry algorithm (exponential backoff + proportional jitter, factor 0.2 fixed). Exposes maxRetries / initialRetryDelayMillis / maxRetryDelayMillis; retries on 408/429/5xx. Carries a TODO to replace with Fern's generated interceptor once a version exposing the delay knobs is vendored. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 expose configurable timeout + retries (client & vault level) - VaultConfig: vault-level timeout/maxRetries/initialRetryDelayMillis/ maxRetryDelayMillis (nullable = inherit) - Skyflow.SkyflowClientBuilder: flat client-wide setters + propagation to existing controllers (mirrors addSkyflowCredentials) - VaultClient.updateExecutorInHTTP: set callTimeout + attach SkyflowRetryInterceptor; per-field precedence vault > client > SDK default (defaults 60s / 3 / 500ms / 2000ms) Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 tests for configurable timeout + retries - SkyflowRetryInterceptorTests: retry count, stop-on-success, 408/429/5xx triggers, non-retryable passthrough, zero-retries - VaultClientHttpConfigTests: callTimeout applied to shared client, precedence (vault > client > default), bounded-not-zero regression guard, VaultConfig field getters/setters Note: pre-existing VaultClientTests#testSetBearerTokenWithEnvCredentials fails on Java 21 (PowerMock byte-buddy cannot instrument JDK security classes) — reproduced on base v3, unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 add sample for configurable timeout + retries Demonstrates client-wide defaults on Skyflow.builder() and per-vault overrides on VaultConfig, with precedence and unit notes. Compiles against the local SDK build (samples module pins a released version, so it builds once this change ships). Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 add MockWebServer integration test (live timeout + retry proof) Drives an OkHttpClient built like VaultClient (callTimeout + SkyflowRetry Interceptor) against a real loopback MockWebServer: - retries 503 then succeeds (server sees 1+retries requests) - exhausts retries, returns last failure - no retry on 400 - callTimeout aborts a never-responding server (~1s, not hanging) -- the fix - observable backoff between retries Adds com.squareup.okhttp3:mockwebserver as a test dependency. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 remove MockWebServer test (breaks CI on PowerMock + Java 21) MockWebServer's static init loads JDK security providers, which the PowerMock/byte-buddy agent fails to instrument on Java 21 (IllegalClassFormatException: Unsupported class file major version 65) -> ExceptionInInitializerError in setUp. This is the same agent/JDK incompatibility that already fails testSetBearerTokenWithEnvCredentials. Retry/timeout behavior remains covered by the mocked-Chain unit tests (SkyflowRetryInterceptorTests) and the config/precedence tests (VaultClientHttpConfigTests), both green in CI. Also drops the mockwebserver test dependency. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 raise coverage for new timeout/retry code Cover the previously-untested new code paths flagged by Codecov: - Skyflow.builder() setters + propagateHttpConfig loop (config set before and after addVaultConfig) -> SkyflowClientBuilderHttpConfigTests - resolveInt/resolveLong vault + client branches via maxRetries + backoff precedence, asserted on the built SkyflowRetryInterceptor's fields - interceptor InterruptedException-during-backoff path (throws IOException, preserves interrupt flag) Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002-retry-backoff-support * SK-3002 use Fern v4 generated RetryInterceptor; rename knobs to initialRetryDelay/maxRetryDelay - Swap hand-written SkyflowRetryInterceptor for Fern's v4 generated RetryInterceptor (public 4-arg ctor: maxRetries, Optional initial/max delay, Optional jitter); delete SkyflowRetryInterceptor + its unit test - Rename public knobs initialRetryDelayMillis/maxRetryDelayMillis -> initialRetryDelay/ maxRetryDelay (Fern naming); still long ms, client + vault levels only - VaultClient attaches the generated RetryInterceptor on the shared client, passing resolved values as Optional (jitter Optional.empty -> Fern default 0.2). Gains Retry-After / X-RateLimit-Reset header awareness for free - Update VaultClientHttpConfigTests to reflect Fern's Duration fields; rename sample Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 fix CI coverage: prepend @{argLine} so JaCoCo agent attaches The v4-regen added a static (--add-opens ...) to maven-surefire-plugin, which overwrote the argLine JaCoCo's prepare-agent injects. The coverage agent therefore never attached -> no jacoco.exec -> "Skipping JaCoCo ... missing execution data" -> no jacoco.xml -> Codecov "No coverage reports found" (build green, 439 tests pass, but upload fails). Prepend @{argLine} (Surefire late-binding of JaCoCo's property) so both the coverage agent and the --add-opens flags apply. Verified locally: jacoco.exec and jacoco.xml are generated again. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 default maxRetries=0 (retries opt-in) to avoid retrying non-idempotent writes Retries fire on 408/429/5xx for all operations regardless of HTTP verb, so auto-retrying a non-idempotent write (e.g. insert without upsert) after a 5xx/timeout could create a duplicate record. Default maxRetries to 0 so the unconfigured SDK does not retry; customers opt in by setting maxRetries. The 60s callTimeout still bounds every call. initialRetryDelay/maxRetryDelay defaults only take effect once maxRetries > 0. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Devesh-Skyflow Co-authored-by: Claude Opus 4.8 (1M context) --- pom.xml | 13 + .../vault/TimeoutAndRetryConfigExample.java | 74 +++ v3/src/main/java/com/skyflow/Skyflow.java | 43 +- v3/src/main/java/com/skyflow/VaultClient.java | 54 +- .../java/com/skyflow/config/VaultConfig.java | 45 ++ .../generated/rest/ApiClientBuilder.java | 194 ++++++- .../generated/rest/AsyncApiClientBuilder.java | 194 ++++++- .../rest/core/ApiClientApiException.java | 6 +- .../generated/rest/core/ClientOptions.java | 128 ++++- .../generated/rest/core/ConsoleLogger.java | 51 ++ .../rest/core/DateTimeDeserializer.java | 21 +- .../generated/rest/core/DoubleSerializer.java | 43 ++ .../skyflow/generated/rest/core/ILogger.java | 38 ++ .../rest/core/InputStreamRequestBody.java | 7 +- .../generated/rest/core/LogConfig.java | 98 ++++ .../skyflow/generated/rest/core/LogLevel.java | 36 ++ .../skyflow/generated/rest/core/Logger.java | 97 ++++ .../rest/core/LoggingInterceptor.java | 104 ++++ .../rest/core/NullableNonemptyFilter.java | 5 +- .../generated/rest/core/ObjectMappers.java | 10 + .../generated/rest/core/RequestOptions.java | 58 +- .../ResponseDecompressionInterceptor.java | 56 ++ .../generated/rest/core/RetryInterceptor.java | 190 ++++++- .../core/Rfc2822DateTimeDeserializer.java | 25 + .../skyflow/generated/rest/core/SseEvent.java | 114 ++++ .../generated/rest/core/SseEventParser.java | 228 ++++++++ .../skyflow/generated/rest/core/Stream.java | 510 ++++++++++++++++-- .../flowservice/AsyncFlowserviceClient.java | 32 ++ .../AsyncRawFlowserviceClient.java | 310 ++++++++--- .../flowservice/FlowserviceClient.java | 32 ++ .../flowservice/RawFlowserviceClient.java | 297 +++++++--- .../flowservice/requests/V1DeleteRequest.java | 10 + .../requests/V1FlowDeleteTokenRequest.java | 10 + .../requests/V1FlowDetokenizeRequest.java | 10 + .../requests/V1FlowTokenizeRequest.java | 10 + .../requests/V1FlowVaultMetricsRequest.java | 10 + .../flowservice/requests/V1GetRequest.java | 10 + .../flowservice/requests/V1InsertRequest.java | 10 + .../flowservice/requests/V1UpdateRequest.java | 10 + .../records/AsyncRawRecordsClient.java | 44 +- .../resources/records/AsyncRecordsClient.java | 7 + .../resources/records/RawRecordsClient.java | 41 +- .../rest/resources/records/RecordsClient.java | 7 + .../requests/V1ExecuteQueryRequest.java | 10 + .../rest/types/FlowEnumUpdateType.java | 73 ++- .../FlowTokenizeResponseObjectToken.java | 10 + .../rest/types/GoogleprotobufAny.java | 10 + .../rest/types/ProtobufNullValue.java | 73 +++ .../generated/rest/types/RpcStatus.java | 10 + .../rest/types/V1ColumnRedactions.java | 10 + .../rest/types/V1DeleteResponse.java | 10 + .../rest/types/V1DeleteResponseObject.java | 10 + .../types/V1DeleteTokenResponseObject.java | 10 + .../types/V1ExecuteQueryRecordResponse.java | 10 + .../rest/types/V1ExecuteQueryResponse.java | 10 + .../types/V1ExecuteQueryResponseMetadata.java | 10 + .../rest/types/V1FlowDeleteTokenResponse.java | 10 + .../rest/types/V1FlowDetokenizeResponse.java | 10 + .../types/V1FlowDetokenizeResponseObject.java | 10 + .../types/V1FlowTokenizeRequestObject.java | 10 + .../rest/types/V1FlowTokenizeResponse.java | 10 + .../types/V1FlowTokenizeResponseObject.java | 10 + .../rest/types/V1FlowVaultMetricsData.java | 10 + .../types/V1FlowVaultMetricsResponse.java | 10 + .../rest/types/V1GetRequestData.java | 10 + .../generated/rest/types/V1GetResponse.java | 10 + .../rest/types/V1InsertRecordData.java | 10 + .../rest/types/V1InsertResponse.java | 10 + .../rest/types/V1RecordResponseObject.java | 10 + .../rest/types/V1TokenGroupRedactions.java | 10 + .../generated/rest/types/V1UniqueValue.java | 10 + .../rest/types/V1UpdateRecordData.java | 10 + .../rest/types/V1UpdateResponse.java | 10 + .../generated/rest/types/V1Upsert.java | 10 + .../SkyflowClientBuilderHttpConfigTests.java | 72 +++ .../skyflow/VaultClientHttpConfigTests.java | 171 ++++++ .../java/com/skyflow/VaultClientTests.java | 8 +- 77 files changed, 3688 insertions(+), 291 deletions(-) create mode 100644 samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/ConsoleLogger.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/DoubleSerializer.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/ILogger.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/LogConfig.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/LogLevel.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/Logger.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/LoggingInterceptor.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/ResponseDecompressionInterceptor.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/Rfc2822DateTimeDeserializer.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/SseEvent.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/core/SseEventParser.java create mode 100644 v3/src/main/java/com/skyflow/generated/rest/types/ProtobufNullValue.java create mode 100644 v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java create mode 100644 v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java diff --git a/pom.xml b/pom.xml index 611ce4e6..0564192b 100644 --- a/pom.xml +++ b/pom.xml @@ -182,6 +182,19 @@ org.apache.maven.plugins maven-surefire-plugin 2.22.2 + + + @{argLine} + --add-opens java.base/java.io=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.util.concurrent=ALL-UNNAMED + --add-opens java.base/java.net=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.base/java.text=ALL-UNNAMED + --add-opens java.base/sun.security.x509=ALL-UNNAMED + + org.jacoco diff --git a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java new file mode 100644 index 00000000..d537df30 --- /dev/null +++ b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java @@ -0,0 +1,74 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; + +/** + * This sample demonstrates how to configure HTTP timeout and retry behavior in the Skyflow Java SDK. + * + *

Configurable settings (all optional): + *

    + *
  • {@code timeout} – overall call timeout in seconds (bounds the whole + * request including retries and backoff). Default: 60.
  • + *
  • {@code maxRetries} – retry attempts after the first failure (retries on HTTP + * 408 / 429 / 5xx). Default: 0 — retries are OFF unless you set this (avoids auto-retrying non-idempotent writes).
  • + *
  • {@code initialRetryDelay} – base backoff before the first retry, in milliseconds. + * Default: 500.
  • + *
  • {@code maxRetryDelay} – cap on the (exponentially growing) backoff, in + * milliseconds. Default: 2000.
  • + *
+ * + *

Two levels + precedence: set client-wide defaults on {@code Skyflow.builder()}, and/or + * per-vault overrides on {@code VaultConfig}. The most specific value wins, resolved per field: + * per-vault → client-wide → SDK default. + */ +public class TimeoutAndRetryConfigExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault. Optionally override timeout/retry settings for THIS vault only. + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + // Per-vault overrides (optional). Any field left unset inherits the client-wide default below, + // and then the SDK default. + vaultConfig.setTimeout(30); // seconds – tighter overall ceiling for this vault + vaultConfig.setMaxRetries(2); // fewer retries for this vault + vaultConfig.setInitialRetryDelay(500L); + vaultConfig.setMaxRetryDelay(1000L); + + // Step 3: Create the Skyflow client. Client-wide defaults apply to every vault + // unless that vault overrides them (as above). + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .timeout(60) // seconds – client-wide overall call timeout + .maxRetries(3) // client-wide retry attempts + .initialRetryDelay(500L) // client-wide base backoff (ms) + .maxRetryDelay(2000L) // client-wide backoff cap (ms) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Use the client as usual. Requests now fail fast at the configured timeout and + // retry transient 408/429/5xx responses with exponential backoff + jitter. + System.out.println("Skyflow client configured with custom timeout & retry settings: " + skyflowClient); + + // Example (uncomment and fill in a real request to try it): + // DetokenizeResponse response = skyflowClient.vault().detokenize(detokenizeRequest); + // System.out.println(response); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the process + System.err.println("Error in Skyflow operations: " + e.getMessage()); + } + } +} diff --git a/v3/src/main/java/com/skyflow/Skyflow.java b/v3/src/main/java/com/skyflow/Skyflow.java index db42fe85..81d3eb5f 100644 --- a/v3/src/main/java/com/skyflow/Skyflow.java +++ b/v3/src/main/java/com/skyflow/Skyflow.java @@ -55,6 +55,11 @@ public VaultController vault() throws SkyflowException { public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder { private final LinkedHashMap vaultConfigMap; private final LinkedHashMap vaultClientsMap; + // Client-wide HTTP config defaults (apply to all vaults unless a vault overrides). null => SDK default. + private Integer timeout; + private Integer maxRetries; + private Long initialRetryDelay; + private Long maxRetryDelay; public SkyflowClientBuilder() { this.vaultConfigMap = new LinkedHashMap<>(); @@ -78,7 +83,9 @@ public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws Skyfl ErrorMessage.VaultIdAlreadyInConfigList.getMessage()); } else { this.vaultConfigMap.put(vaultConfigCopy.getVaultId(), vaultConfigCopy); // add new config in map - this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), new VaultController(vaultConfigCopy, this.skyflowCredentials)); // add new controller with new config + VaultController controller = new VaultController(vaultConfigCopy, this.skyflowCredentials); // new controller with new config + controller.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelay, this.maxRetryDelay); + this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), controller); LogUtil.printInfoLog(Utils.parameterizedString( InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfigCopy.getVaultId())); } @@ -100,6 +107,40 @@ public SkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throw return this; } + /** Client-wide overall call timeout in seconds. Applies to all vaults unless a vault overrides it. */ + public SkyflowClientBuilder timeout(int timeout) { + this.timeout = timeout; + propagateHttpConfig(); + return this; + } + + /** Client-wide retry attempt count. Applies to all vaults unless a vault overrides it. */ + public SkyflowClientBuilder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + propagateHttpConfig(); + return this; + } + + /** Client-wide base retry backoff in milliseconds. Applies to all vaults unless a vault overrides it. */ + public SkyflowClientBuilder initialRetryDelay(long initialRetryDelay) { + this.initialRetryDelay = initialRetryDelay; + propagateHttpConfig(); + return this; + } + + /** Client-wide retry backoff cap in milliseconds. Applies to all vaults unless a vault overrides it. */ + public SkyflowClientBuilder maxRetryDelay(long maxRetryDelay) { + this.maxRetryDelay = maxRetryDelay; + propagateHttpConfig(); + return this; + } + + private void propagateHttpConfig() { + for (VaultController vault : this.vaultClientsMap.values()) { + vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelay, this.maxRetryDelay); + } + } + @Override public SkyflowClientBuilder setLogLevel(LogLevel logLevel) { super.setLogLevel(logLevel); diff --git a/v3/src/main/java/com/skyflow/VaultClient.java b/v3/src/main/java/com/skyflow/VaultClient.java index f7319d29..2540bb70 100644 --- a/v3/src/main/java/com/skyflow/VaultClient.java +++ b/v3/src/main/java/com/skyflow/VaultClient.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import com.skyflow.config.Credentials; @@ -19,6 +20,7 @@ import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.InfoLogs; import com.skyflow.serviceaccount.util.Token; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import com.skyflow.utils.logger.LogUtil; @@ -45,6 +47,16 @@ public class VaultClient { private String apiKey; private OkHttpClient sharedHttpClient = null; private String currentVaultURL = null; + // Client-wide (Skyflow builder) HTTP config defaults; null => fall back to the SDK defaults below. + private Integer commonTimeout; + private Integer commonMaxRetries; + private Long commonInitialRetryDelay; + private Long commonMaxRetryDelay; + // SDK defaults, used when neither the vault-level nor the client-wide value is set. + private static final int DEFAULT_TIMEOUT_SECONDS = 60; + private static final int DEFAULT_MAX_RETRIES = 0; // retries OFF by default (opt-in) so non-idempotent writes aren't auto-retried + private static final long DEFAULT_INITIAL_RETRY_DELAY_MILLIS = 500L; + private static final long DEFAULT_MAX_RETRY_DELAY_MILLIS = 2000L; protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException { super(); @@ -68,6 +80,20 @@ protected void setCommonCredentials(Credentials commonCredentials) throws Skyflo prioritiseCredentials(); } + /** + * Client-wide HTTP timeout/retry defaults from the Skyflow builder. Nulls out the cached client + * so the next call rebuilds with the new values. + */ + protected void setCommonHttpConfig(Integer timeout, Integer maxRetries, + Long initialRetryDelay, Long maxRetryDelay) { + this.commonTimeout = timeout; + this.commonMaxRetries = maxRetries; + this.commonInitialRetryDelay = initialRetryDelay; + this.commonMaxRetryDelay = maxRetryDelay; + this.sharedHttpClient = null; + this.apiClient = null; + } + protected void setBearerToken() throws SkyflowException { prioritiseCredentials(); Validations.validateCredentials(this.finalCredentials); @@ -143,9 +169,19 @@ private void prioritiseCredentials() throws SkyflowException { protected void updateExecutorInHTTP() { if (sharedHttpClient == null) { + int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS); + int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES); + long initialRetryDelay = resolveLong( + vaultConfig.getInitialRetryDelay(), commonInitialRetryDelay, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); + long maxRetryDelay = resolveLong( + vaultConfig.getMaxRetryDelay(), commonMaxRetryDelay, DEFAULT_MAX_RETRY_DELAY_MILLIS); + sharedHttpClient = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) - .addInterceptor(chain -> { + .callTimeout(timeoutSeconds, TimeUnit.SECONDS) // overall ceiling; bounds the whole call incl. retries + .addInterceptor(new RetryInterceptor( // OUTER: retries (Fern generated; jitter default 0.2) + maxRetries, Optional.of(initialRetryDelay), Optional.of(maxRetryDelay), Optional.empty())) + .addInterceptor(chain -> { // INNER: auth (reads this.token per request) Request requestWithAuth = chain.request().newBuilder() .header("Authorization", "Bearer " + this.token) .build(); @@ -156,6 +192,22 @@ protected void updateExecutorInHTTP() { } } + /** Resolve an int setting: vault-level override, else client-wide default, else SDK default. */ + private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defaultValue) { + if (vaultLevel != null) { + return vaultLevel; + } + return clientLevel != null ? clientLevel : defaultValue; + } + + /** Resolve a long setting: vault-level override, else client-wide default, else SDK default. */ + private static long resolveLong(Long vaultLevel, Long clientLevel, long defaultValue) { + if (vaultLevel != null) { + return vaultLevel; + } + return clientLevel != null ? clientLevel : defaultValue; + } + protected V1InsertRequest getBulkInsertRequestBody(com.skyflow.vault.data.InsertRequest request, VaultConfig config) { ArrayList records = request.getRecords(); List insertRecordDataList = new ArrayList<>(); diff --git a/v3/src/main/java/com/skyflow/config/VaultConfig.java b/v3/src/main/java/com/skyflow/config/VaultConfig.java index 23ce7a51..69bec4d0 100644 --- a/v3/src/main/java/com/skyflow/config/VaultConfig.java +++ b/v3/src/main/java/com/skyflow/config/VaultConfig.java @@ -8,6 +8,11 @@ public class VaultConfig implements Cloneable { private String vaultURL; private Env env; private Credentials credentials; + // HTTP timeout & retry config (vault-level overrides). null => inherit client-wide default, then SDK default. + private Integer timeout; // overall call timeout, in seconds + private Integer maxRetries; // retry attempts after the first failure + private Long initialRetryDelay; // base backoff before the first retry, in ms + private Long maxRetryDelay; // cap on the (exponentially growing) backoff, in ms public VaultConfig() { this.vaultId = null; @@ -15,6 +20,10 @@ public VaultConfig() { this.vaultURL = null; this.env = Env.PROD; this.credentials = null; + this.timeout = null; + this.maxRetries = null; + this.initialRetryDelay = null; + this.maxRetryDelay = null; } public String getVaultId() { @@ -57,6 +66,42 @@ public void setVaultURL(String vaultURL) { this.vaultURL = vaultURL; } + public Integer getTimeout() { + return timeout; + } + + /** Overall call timeout in seconds for this vault. Overrides the client-wide default. */ + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getMaxRetries() { + return maxRetries; + } + + /** Number of retry attempts after the first failure for this vault. Overrides the client-wide default. */ + public void setMaxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + } + + public Long getInitialRetryDelay() { + return initialRetryDelay; + } + + /** Base retry backoff in milliseconds for this vault. Overrides the client-wide default. */ + public void setInitialRetryDelay(Long initialRetryDelay) { + this.initialRetryDelay = initialRetryDelay; + } + + public Long getMaxRetryDelay() { + return maxRetryDelay; + } + + /** Cap on retry backoff in milliseconds for this vault. Overrides the client-wide default. */ + public void setMaxRetryDelay(Long maxRetryDelay) { + this.maxRetryDelay = maxRetryDelay; + } + @Override public Object clone() throws CloneNotSupportedException { VaultConfig cloned = (VaultConfig) super.clone(); diff --git a/v3/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java b/v3/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java index fa3d6e9d..b2c312d7 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java +++ b/v3/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java @@ -5,13 +5,31 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.Environment; +import com.skyflow.generated.rest.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import okhttp3.OkHttpClient; -public final class ApiClientBuilder { - private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); +public class ApiClientBuilder { + 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 Environment environment; + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + public ApiClientBuilder url(String url) { this.environment = Environment.custom(url); return this; @@ -21,7 +39,7 @@ public ApiClientBuilder url(String url) { * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. */ public ApiClientBuilder timeout(int timeout) { - this.clientOptionsBuilder.timeout(timeout); + this.timeout = Optional.of(timeout); return this; } @@ -29,7 +47,31 @@ public ApiClientBuilder timeout(int timeout) { * Sets the maximum number of retries for the client. Defaults to 2 retries. */ public ApiClientBuilder maxRetries(int maxRetries) { - this.clientOptionsBuilder.maxRetries(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 ApiClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public ApiClientBuilder 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 ApiClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); return this; } @@ -37,12 +79,150 @@ public ApiClientBuilder maxRetries(int maxRetries) { * Sets the underlying OkHttp client */ public ApiClientBuilder httpClient(OkHttpClient httpClient) { - this.clientOptionsBuilder.httpClient(httpClient); + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public ApiClientBuilder 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 ApiClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); return this; } + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(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); + } + + /** + * 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 ApiClient build() { - clientOptionsBuilder.environment(this.environment); - return new ApiClient(clientOptionsBuilder.build()); + validateConfiguration(); + return new ApiClient(buildClientOptions()); } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java b/v3/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java index 10c08d51..30c3dfac 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java +++ b/v3/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java @@ -5,13 +5,31 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.Environment; +import com.skyflow.generated.rest.core.LogConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import okhttp3.OkHttpClient; -public final class AsyncApiClientBuilder { - private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); +public class AsyncApiClientBuilder { + 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 Environment environment; + private OkHttpClient httpClient; + + private Optional logging = Optional.empty(); + public AsyncApiClientBuilder url(String url) { this.environment = Environment.custom(url); return this; @@ -21,7 +39,7 @@ public AsyncApiClientBuilder url(String url) { * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. */ public AsyncApiClientBuilder timeout(int timeout) { - this.clientOptionsBuilder.timeout(timeout); + this.timeout = Optional.of(timeout); return this; } @@ -29,7 +47,31 @@ public AsyncApiClientBuilder timeout(int timeout) { * Sets the maximum number of retries for the client. Defaults to 2 retries. */ public AsyncApiClientBuilder maxRetries(int maxRetries) { - this.clientOptionsBuilder.maxRetries(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 AsyncApiClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = Optional.of(initialRetryDelayMillis); + return this; + } + + /** + * Sets the maximum delay (in milliseconds) between retries. Defaults to 60000 milliseconds. + */ + public AsyncApiClientBuilder 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 AsyncApiClientBuilder retryJitterFactor(double retryJitterFactor) { + this.retryJitterFactor = Optional.of(retryJitterFactor); return this; } @@ -37,12 +79,150 @@ public AsyncApiClientBuilder maxRetries(int maxRetries) { * Sets the underlying OkHttp client */ public AsyncApiClientBuilder httpClient(OkHttpClient httpClient) { - this.clientOptionsBuilder.httpClient(httpClient); + this.httpClient = httpClient; + return this; + } + + /** + * Configure logging for the SDK. Silent by default — no log output unless explicitly configured. + */ + public AsyncApiClientBuilder 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 AsyncApiClientBuilder addHeader(String name, String value) { + this.customHeaders.put(name, value); return this; } + protected ClientOptions buildClientOptions() { + ClientOptions.Builder builder = ClientOptions.builder(); + setEnvironment(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); + } + + /** + * 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 AsyncApiClient build() { - clientOptionsBuilder.environment(this.environment); - return new AsyncApiClient(clientOptionsBuilder.build()); + validateConfiguration(); + return new AsyncApiClient(buildClientOptions()); } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java b/v3/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java index a4487b1e..c016a435 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java @@ -65,9 +65,9 @@ public Map> headers() { return this.headers; } - @java.lang.Override + @Override public String toString() { - return "ApiClientApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + ", body: " + body - + "}"; + return "ApiClientApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + ", body: " + + ObjectMappers.stringify(body) + "}"; } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java b/v3/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java index a5d27b01..7d668945 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java @@ -21,12 +21,27 @@ public final class ClientOptions { 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 timeout, + int maxRetries, + Optional initialRetryDelayMillis, + Optional maxRetryDelayMillis, + Optional retryJitterFactor, + Optional logging) { this.environment = environment; this.headers = new HashMap<>(); this.headers.putAll(headers); @@ -40,6 +55,11 @@ private ClientOptions( 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() { @@ -81,11 +101,31 @@ public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { .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 final class Builder { + public static class Builder { private Environment environment; private final Map headers = new HashMap<>(); @@ -94,17 +134,27 @@ public static final class Builder { 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); + if (value != null) { + this.headers.put(key, value); + } return this; } @@ -137,11 +187,43 @@ public Builder maxRetries(int 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(); @@ -158,13 +240,49 @@ public ClientOptions build() { .connectTimeout(0, TimeUnit.SECONDS) .writeTimeout(0, TimeUnit.SECONDS) .readTimeout(0, TimeUnit.SECONDS) - .addInterceptor(new RetryInterceptor(this.maxRetries)); + .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()); + 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/v3/src/main/java/com/skyflow/generated/rest/core/ConsoleLogger.java b/v3/src/main/java/com/skyflow/generated/rest/core/ConsoleLogger.java new file mode 100644 index 00000000..5935ff4c --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ConsoleLogger.java @@ -0,0 +1,51 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java b/v3/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java index 6be10979..0f422bc2 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java @@ -14,11 +14,13 @@ 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 ISO8601 dates into {@link OffsetDateTime} objects. + * 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; @@ -42,8 +44,21 @@ public OffsetDateTime deserialize(JsonParser parser, DeserializationContext cont if (token == JsonToken.VALUE_NUMBER_INT) { return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); } else { - TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( - parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + 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); diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/DoubleSerializer.java b/v3/src/main/java/com/skyflow/generated/rest/core/DoubleSerializer.java new file mode 100644 index 00000000..131d0a9a --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/DoubleSerializer.java @@ -0,0 +1,43 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/ILogger.java b/v3/src/main/java/com/skyflow/generated/rest/core/ILogger.java new file mode 100644 index 00000000..a70ec8e6 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ILogger.java @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java b/v3/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java index 545f6088..5351a5f8 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java @@ -8,7 +8,6 @@ import java.util.Objects; import okhttp3.MediaType; import okhttp3.RequestBody; -import okhttp3.internal.Util; import okio.BufferedSink; import okio.Okio; import okio.Source; @@ -68,12 +67,8 @@ public long contentLength() throws IOException { */ @Override public void writeTo(BufferedSink sink) throws IOException { - Source source = null; - try { - source = Okio.source(inputStream); + try (Source source = Okio.source(inputStream)) { sink.writeAll(source); - } finally { - Util.closeQuietly(Objects.requireNonNull(source)); } } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/LogConfig.java b/v3/src/main/java/com/skyflow/generated/rest/core/LogConfig.java new file mode 100644 index 00000000..7928e097 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/LogConfig.java @@ -0,0 +1,98 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/LogLevel.java b/v3/src/main/java/com/skyflow/generated/rest/core/LogLevel.java new file mode 100644 index 00000000..db72cd0a --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/LogLevel.java @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/Logger.java b/v3/src/main/java/com/skyflow/generated/rest/core/Logger.java new file mode 100644 index 00000000..a5603955 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/Logger.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/LoggingInterceptor.java b/v3/src/main/java/com/skyflow/generated/rest/core/LoggingInterceptor.java new file mode 100644 index 00000000..407b7937 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/LoggingInterceptor.java @@ -0,0 +1,104 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java b/v3/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java index 98c33be4..a6cffd4d 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java @@ -14,6 +14,9 @@ public boolean equals(Object o) { } private boolean isOptionalEmpty(Object o) { - return o instanceof Optional && !((Optional) o).isPresent(); + if (o instanceof Optional) { + return !((Optional) o).isPresent(); + } + return false; } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java b/v3/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java index 3b7894e0..4f69f150 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java @@ -4,6 +4,7 @@ package com.skyflow.generated.rest.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; @@ -17,6 +18,7 @@ public final class ObjectMappers { .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(); @@ -33,4 +35,12 @@ public static String stringify(Object o) { 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/v3/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java b/v3/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java index b8a8d14b..c890c7de 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java @@ -14,19 +14,31 @@ public final class RequestOptions { 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( Optional timeout, TimeUnit timeoutTimeUnit, + Optional maxRetries, Map headers, - Map> headerSuppliers) { + Map> headerSuppliers, + Map queryParameters, + Map> queryParameterSuppliers) { this.timeout = timeout; this.timeoutTimeUnit = timeoutTimeUnit; + this.maxRetries = maxRetries; this.headers = headers; this.headerSuppliers = headerSuppliers; + this.queryParameters = queryParameters; + this.queryParameterSuppliers = queryParameterSuppliers; } public Optional getTimeout() { @@ -37,6 +49,10 @@ public TimeUnit getTimeoutTimeUnit() { return timeoutTimeUnit; } + public Optional getMaxRetries() { + return maxRetries; + } + public Map getHeaders() { Map headers = new HashMap<>(); headers.putAll(this.headers); @@ -46,19 +62,33 @@ public Map getHeaders() { 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 final class Builder { + public static class Builder { 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 timeout(Integer timeout) { this.timeout = Optional.of(timeout); return this; @@ -70,6 +100,11 @@ public Builder timeout(Integer timeout, TimeUnit 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; @@ -80,8 +115,25 @@ public Builder addHeader(String key, Supplier 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(timeout, timeoutTimeUnit, headers, headerSuppliers); + return new RequestOptions( + timeout, + timeoutTimeUnit, + maxRetries, + headers, + headerSuppliers, + queryParameters, + queryParameterSuppliers); } } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/ResponseDecompressionInterceptor.java b/v3/src/main/java/com/skyflow/generated/rest/core/ResponseDecompressionInterceptor.java new file mode 100644 index 00000000..d6938347 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/ResponseDecompressionInterceptor.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java b/v3/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java index eda7d265..7d89751d 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java @@ -5,34 +5,78 @@ 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 ONE_SECOND = Duration.ofSeconds(1); - private final ExponentialBackoff backoff; + 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.backoff = new ExponentialBackoff(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 { - Response response = chain.proceed(chain.request()); + Request request = chain.request(); + int effectiveMaxRetries = resolveMaxRetries(request); + Response response = chain.proceed(request); if (shouldRetry(response.code())) { - return retryChain(response, chain); + return retryChain(response, chain, effectiveMaxRetries); } return response; } - private Response retryChain(Response response, Chain chain) throws IOException { - Optional nextBackoff = this.backoff.nextBackoff(); + 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()); @@ -42,7 +86,7 @@ private Response retryChain(Response response, Chain chain) throws IOException { response.close(); response = chain.proceed(chain.request()); if (shouldRetry(response.code())) { - nextBackoff = this.backoff.nextBackoff(); + nextBackoff = backoff.nextBackoff(response); } else { return response; } @@ -51,10 +95,130 @@ private Response retryChain(Response response, Chain chain) throws IOException { 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; @@ -65,14 +229,14 @@ private final class ExponentialBackoff { this.maxNumRetries = maxNumRetries; } - public Optional nextBackoff() { - retryNumber += 1; - if (retryNumber > maxNumRetries) { + public Optional nextBackoff(Response response) { + if (retryNumber >= maxNumRetries) { return Optional.empty(); } - int upperBound = (int) Math.pow(2, retryNumber); - return Optional.of(ONE_SECOND.multipliedBy(random.nextInt(upperBound))); + Duration delay = getRetryDelayFromHeaders(response, retryNumber); + retryNumber += 1; + return Optional.of(delay); } } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/core/Rfc2822DateTimeDeserializer.java b/v3/src/main/java/com/skyflow/generated/rest/core/Rfc2822DateTimeDeserializer.java new file mode 100644 index 00000000..3a826210 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/Rfc2822DateTimeDeserializer.java @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/SseEvent.java b/v3/src/main/java/com/skyflow/generated/rest/core/SseEvent.java new file mode 100644 index 00000000..9ab823d5 --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/SseEvent.java @@ -0,0 +1,114 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/SseEventParser.java b/v3/src/main/java/com/skyflow/generated/rest/core/SseEventParser.java new file mode 100644 index 00000000..ce50f86b --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/core/SseEventParser.java @@ -0,0 +1,228 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.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/v3/src/main/java/com/skyflow/generated/rest/core/Stream.java b/v3/src/main/java/com/skyflow/generated/rest/core/Stream.java index f037712a..3822aa81 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/core/Stream.java +++ b/v3/src/main/java/com/skyflow/generated/rest/core/Stream.java @@ -3,6 +3,8 @@ */ package com.skyflow.generated.rest.core; +import java.io.Closeable; +import java.io.IOException; import java.io.Reader; import java.util.Iterator; import java.util.NoSuchElementException; @@ -14,18 +16,30 @@ *

* {@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 { - /** - * The {@link Class} of the 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; - /** - * The {@link Scanner} used for reading from the input stream and blocking when needed during iteration. - */ 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. @@ -35,8 +49,100 @@ public final class Stream implements Iterable { * @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; } /** @@ -47,51 +153,361 @@ public Stream(Class valueType, Reader reader, String delimiter) { */ @Override public Iterator iterator() { - return new 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() { - 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 (!scanner.hasNext()) { - throw new NoSuchElementException(); - } else { + 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 { - T parsedResponse = ObjectMappers.JSON_MAPPER.readValue( - scanner.next().trim(), valueType); - return parsedResponse; - } catch (Exception e) { - throw new RuntimeException(e); + 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; } - /** - * Removing elements from {@code Stream} is not supported. - * - * @throws UnsupportedOperationException Always, as removal is not supported. - */ - @Override - public void remove() { - throw new UnsupportedOperationException(); + 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java index 195490a6..93fcae89 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java @@ -44,6 +44,10 @@ public CompletableFuture delete() { return this.rawClient.delete().thenApply(response -> response.body()); } + public CompletableFuture delete(RequestOptions requestOptions) { + return this.rawClient.delete(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture delete(V1DeleteRequest request) { return this.rawClient.delete(request).thenApply(response -> response.body()); } @@ -56,6 +60,10 @@ public CompletableFuture get() { return this.rawClient.get().thenApply(response -> response.body()); } + public CompletableFuture get(RequestOptions requestOptions) { + return this.rawClient.get(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture get(V1GetRequest request) { return this.rawClient.get(request).thenApply(response -> response.body()); } @@ -68,6 +76,10 @@ public CompletableFuture insert() { return this.rawClient.insert().thenApply(response -> response.body()); } + public CompletableFuture insert(RequestOptions requestOptions) { + return this.rawClient.insert(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture insert(V1InsertRequest request) { return this.rawClient.insert(request).thenApply(response -> response.body()); } @@ -80,6 +92,10 @@ public CompletableFuture update() { return this.rawClient.update().thenApply(response -> response.body()); } + public CompletableFuture update(RequestOptions requestOptions) { + return this.rawClient.update(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture update(V1UpdateRequest request) { return this.rawClient.update(request).thenApply(response -> response.body()); } @@ -92,6 +108,10 @@ public CompletableFuture deletetoken() { return this.rawClient.deletetoken().thenApply(response -> response.body()); } + public CompletableFuture deletetoken(RequestOptions requestOptions) { + return this.rawClient.deletetoken(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture deletetoken(V1FlowDeleteTokenRequest request) { return this.rawClient.deletetoken(request).thenApply(response -> response.body()); } @@ -105,6 +125,10 @@ public CompletableFuture detokenize() { return this.rawClient.detokenize().thenApply(response -> response.body()); } + public CompletableFuture detokenize(RequestOptions requestOptions) { + return this.rawClient.detokenize(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture detokenize(V1FlowDetokenizeRequest request) { return this.rawClient.detokenize(request).thenApply(response -> response.body()); } @@ -118,6 +142,10 @@ public CompletableFuture tokenize() { return this.rawClient.tokenize().thenApply(response -> response.body()); } + public CompletableFuture tokenize(RequestOptions requestOptions) { + return this.rawClient.tokenize(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture tokenize(V1FlowTokenizeRequest request) { return this.rawClient.tokenize(request).thenApply(response -> response.body()); } @@ -131,6 +159,10 @@ public CompletableFuture flowvaultmetrics() { return this.rawClient.flowvaultmetrics().thenApply(response -> response.body()); } + public CompletableFuture flowvaultmetrics(RequestOptions requestOptions) { + return this.rawClient.flowvaultmetrics(requestOptions).thenApply(response -> response.body()); + } + public CompletableFuture flowvaultmetrics(V1FlowVaultMetricsRequest request) { return this.rawClient.flowvaultmetrics(request).thenApply(response -> response.body()); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java index 0ddf04f8..3df4536c 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java @@ -11,6 +11,7 @@ import com.skyflow.generated.rest.core.MediaTypes; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; @@ -51,16 +52,24 @@ public CompletableFuture> delete() { return delete(V1DeleteRequest.builder().build()); } + public CompletableFuture> delete(RequestOptions requestOptions) { + return delete(V1DeleteRequest.builder().build(), requestOptions); + } + public CompletableFuture> delete(V1DeleteRequest request) { return delete(request, null); } public CompletableFuture> delete( V1DeleteRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/delete") - .build(); + .addPathSegments("v2/records/delete"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -69,7 +78,7 @@ public CompletableFuture> delete( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -79,24 +88,34 @@ public CompletableFuture> delete( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1DeleteResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1DeleteResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -114,16 +133,24 @@ public CompletableFuture> get() { return get(V1GetRequest.builder().build()); } + public CompletableFuture> get(RequestOptions requestOptions) { + return get(V1GetRequest.builder().build(), requestOptions); + } + public CompletableFuture> get(V1GetRequest request) { return get(request, null); } public CompletableFuture> get( V1GetRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/get") - .build(); + .addPathSegments("v2/records/get"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -132,7 +159,7 @@ public CompletableFuture> get( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -142,24 +169,34 @@ public CompletableFuture> get( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1GetResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1GetResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -177,16 +214,24 @@ public CompletableFuture> insert() { return insert(V1InsertRequest.builder().build()); } + public CompletableFuture> insert(RequestOptions requestOptions) { + return insert(V1InsertRequest.builder().build(), requestOptions); + } + public CompletableFuture> insert(V1InsertRequest request) { return insert(request, null); } public CompletableFuture> insert( V1InsertRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/insert") - .build(); + .addPathSegments("v2/records/insert"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -195,7 +240,7 @@ public CompletableFuture> insert( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -205,24 +250,34 @@ public CompletableFuture> insert( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1InsertResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1InsertResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -240,16 +295,24 @@ public CompletableFuture> update() { return update(V1UpdateRequest.builder().build()); } + public CompletableFuture> update(RequestOptions requestOptions) { + return update(V1UpdateRequest.builder().build(), requestOptions); + } + public CompletableFuture> update(V1UpdateRequest request) { return update(request, null); } public CompletableFuture> update( V1UpdateRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/update") - .build(); + .addPathSegments("v2/records/update"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -258,7 +321,7 @@ public CompletableFuture> update( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -268,24 +331,34 @@ public CompletableFuture> update( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1UpdateResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1UpdateResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -303,6 +376,11 @@ public CompletableFuture> delet return deletetoken(V1FlowDeleteTokenRequest.builder().build()); } + public CompletableFuture> deletetoken( + RequestOptions requestOptions) { + return deletetoken(V1FlowDeleteTokenRequest.builder().build(), requestOptions); + } + public CompletableFuture> deletetoken( V1FlowDeleteTokenRequest request) { return deletetoken(request, null); @@ -310,10 +388,14 @@ public CompletableFuture> delet public CompletableFuture> deletetoken( V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/delete") - .build(); + .addPathSegments("v2/tokens/delete"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -322,7 +404,7 @@ public CompletableFuture> delet throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -332,25 +414,35 @@ public CompletableFuture> delet 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 ApiClientHttpResponse<>( ObjectMappers.JSON_MAPPER.readValue( - responseBody.string(), V1FlowDeleteTokenResponse.class), + responseBodyString, V1FlowDeleteTokenResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -368,6 +460,11 @@ public CompletableFuture> detoke return detokenize(V1FlowDetokenizeRequest.builder().build()); } + public CompletableFuture> detokenize( + RequestOptions requestOptions) { + return detokenize(V1FlowDetokenizeRequest.builder().build(), requestOptions); + } + public CompletableFuture> detokenize( V1FlowDetokenizeRequest request) { return detokenize(request, null); @@ -375,10 +472,14 @@ public CompletableFuture> detoke public CompletableFuture> detokenize( V1FlowDetokenizeRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/detokenize") - .build(); + .addPathSegments("v2/tokens/detokenize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -387,7 +488,7 @@ public CompletableFuture> detoke throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -397,25 +498,34 @@ public CompletableFuture> detoke 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue( - responseBody.string(), V1FlowDetokenizeResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowDetokenizeResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -433,16 +543,24 @@ public CompletableFuture> tokenize return tokenize(V1FlowTokenizeRequest.builder().build()); } + public CompletableFuture> tokenize(RequestOptions requestOptions) { + return tokenize(V1FlowTokenizeRequest.builder().build(), requestOptions); + } + public CompletableFuture> tokenize(V1FlowTokenizeRequest request) { return tokenize(request, null); } public CompletableFuture> tokenize( V1FlowTokenizeRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/tokenize") - .build(); + .addPathSegments("v2/tokens/tokenize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -451,7 +569,7 @@ public CompletableFuture> tokenize throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -461,25 +579,34 @@ public CompletableFuture> tokenize 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue( - responseBody.string(), V1FlowTokenizeResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowTokenizeResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } @@ -497,6 +624,11 @@ public CompletableFuture> flow return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build()); } + public CompletableFuture> flowvaultmetrics( + RequestOptions requestOptions) { + return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build(), requestOptions); + } + public CompletableFuture> flowvaultmetrics( V1FlowVaultMetricsRequest request) { return flowvaultmetrics(request, null); @@ -504,10 +636,14 @@ public CompletableFuture> flow public CompletableFuture> flowvaultmetrics( V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/vaults/metrics") - .build(); + .addPathSegments("v2/vaults/metrics"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -516,7 +652,7 @@ public CompletableFuture> flow throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -526,25 +662,35 @@ public CompletableFuture> flow 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 ApiClientHttpResponse<>( ObjectMappers.JSON_MAPPER.readValue( - responseBody.string(), V1FlowVaultMetricsResponse.class), + responseBodyString, V1FlowVaultMetricsResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java index 510c32b4..0e9a98b7 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java @@ -43,6 +43,10 @@ public V1DeleteResponse delete() { return this.rawClient.delete().body(); } + public V1DeleteResponse delete(RequestOptions requestOptions) { + return this.rawClient.delete(requestOptions).body(); + } + public V1DeleteResponse delete(V1DeleteRequest request) { return this.rawClient.delete(request).body(); } @@ -55,6 +59,10 @@ public V1GetResponse get() { return this.rawClient.get().body(); } + public V1GetResponse get(RequestOptions requestOptions) { + return this.rawClient.get(requestOptions).body(); + } + public V1GetResponse get(V1GetRequest request) { return this.rawClient.get(request).body(); } @@ -67,6 +75,10 @@ public V1InsertResponse insert() { return this.rawClient.insert().body(); } + public V1InsertResponse insert(RequestOptions requestOptions) { + return this.rawClient.insert(requestOptions).body(); + } + public V1InsertResponse insert(V1InsertRequest request) { return this.rawClient.insert(request).body(); } @@ -79,6 +91,10 @@ public V1UpdateResponse update() { return this.rawClient.update().body(); } + public V1UpdateResponse update(RequestOptions requestOptions) { + return this.rawClient.update(requestOptions).body(); + } + public V1UpdateResponse update(V1UpdateRequest request) { return this.rawClient.update(request).body(); } @@ -91,6 +107,10 @@ public V1FlowDeleteTokenResponse deletetoken() { return this.rawClient.deletetoken().body(); } + public V1FlowDeleteTokenResponse deletetoken(RequestOptions requestOptions) { + return this.rawClient.deletetoken(requestOptions).body(); + } + public V1FlowDeleteTokenResponse deletetoken(V1FlowDeleteTokenRequest request) { return this.rawClient.deletetoken(request).body(); } @@ -103,6 +123,10 @@ public V1FlowDetokenizeResponse detokenize() { return this.rawClient.detokenize().body(); } + public V1FlowDetokenizeResponse detokenize(RequestOptions requestOptions) { + return this.rawClient.detokenize(requestOptions).body(); + } + public V1FlowDetokenizeResponse detokenize(V1FlowDetokenizeRequest request) { return this.rawClient.detokenize(request).body(); } @@ -115,6 +139,10 @@ public V1FlowTokenizeResponse tokenize() { return this.rawClient.tokenize().body(); } + public V1FlowTokenizeResponse tokenize(RequestOptions requestOptions) { + return this.rawClient.tokenize(requestOptions).body(); + } + public V1FlowTokenizeResponse tokenize(V1FlowTokenizeRequest request) { return this.rawClient.tokenize(request).body(); } @@ -127,6 +155,10 @@ public V1FlowVaultMetricsResponse flowvaultmetrics() { return this.rawClient.flowvaultmetrics().body(); } + public V1FlowVaultMetricsResponse flowvaultmetrics(RequestOptions requestOptions) { + return this.rawClient.flowvaultmetrics(requestOptions).body(); + } + public V1FlowVaultMetricsResponse flowvaultmetrics(V1FlowVaultMetricsRequest request) { return this.rawClient.flowvaultmetrics(request).body(); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java index ca930b6f..9a3a3b6f 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java @@ -11,6 +11,7 @@ import com.skyflow.generated.rest.core.MediaTypes; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; @@ -47,15 +48,23 @@ public ApiClientHttpResponse delete() { return delete(V1DeleteRequest.builder().build()); } + public ApiClientHttpResponse delete(RequestOptions requestOptions) { + return delete(V1DeleteRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse delete(V1DeleteRequest request) { return delete(request, null); } public ApiClientHttpResponse delete(V1DeleteRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/delete") - .build(); + .addPathSegments("v2/records/delete"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -64,7 +73,7 @@ public ApiClientHttpResponse delete(V1DeleteRequest request, R throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -74,18 +83,27 @@ public ApiClientHttpResponse delete(V1DeleteRequest request, R 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1DeleteResponse.class), response); + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1DeleteResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -95,15 +113,23 @@ public ApiClientHttpResponse get() { return get(V1GetRequest.builder().build()); } + public ApiClientHttpResponse get(RequestOptions requestOptions) { + return get(V1GetRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse get(V1GetRequest request) { return get(request, null); } public ApiClientHttpResponse get(V1GetRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/get") - .build(); + .addPathSegments("v2/records/get"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -112,7 +138,7 @@ public ApiClientHttpResponse get(V1GetRequest request, RequestOpt throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -122,18 +148,27 @@ public ApiClientHttpResponse get(V1GetRequest request, RequestOpt 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1GetResponse.class), response); + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1GetResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -143,15 +178,23 @@ public ApiClientHttpResponse insert() { return insert(V1InsertRequest.builder().build()); } + public ApiClientHttpResponse insert(RequestOptions requestOptions) { + return insert(V1InsertRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse insert(V1InsertRequest request) { return insert(request, null); } public ApiClientHttpResponse insert(V1InsertRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/insert") - .build(); + .addPathSegments("v2/records/insert"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -160,7 +203,7 @@ public ApiClientHttpResponse insert(V1InsertRequest request, R throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -170,18 +213,27 @@ public ApiClientHttpResponse insert(V1InsertRequest request, R 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1InsertResponse.class), response); + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1InsertResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -191,15 +243,23 @@ public ApiClientHttpResponse update() { return update(V1UpdateRequest.builder().build()); } + public ApiClientHttpResponse update(RequestOptions requestOptions) { + return update(V1UpdateRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse update(V1UpdateRequest request) { return update(request, null); } public ApiClientHttpResponse update(V1UpdateRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/records/update") - .build(); + .addPathSegments("v2/records/update"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -208,7 +268,7 @@ public ApiClientHttpResponse update(V1UpdateRequest request, R throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -218,18 +278,27 @@ public ApiClientHttpResponse update(V1UpdateRequest request, R 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1UpdateResponse.class), response); + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1UpdateResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -239,16 +308,24 @@ public ApiClientHttpResponse deletetoken() { return deletetoken(V1FlowDeleteTokenRequest.builder().build()); } + public ApiClientHttpResponse deletetoken(RequestOptions requestOptions) { + return deletetoken(V1FlowDeleteTokenRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse deletetoken(V1FlowDeleteTokenRequest request) { return deletetoken(request, null); } public ApiClientHttpResponse deletetoken( V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/delete") - .build(); + .addPathSegments("v2/tokens/delete"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -257,7 +334,7 @@ public ApiClientHttpResponse deletetoken( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -267,19 +344,28 @@ public ApiClientHttpResponse deletetoken( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowDeleteTokenResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowDeleteTokenResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -289,16 +375,24 @@ public ApiClientHttpResponse detokenize() { return detokenize(V1FlowDetokenizeRequest.builder().build()); } + public ApiClientHttpResponse detokenize(RequestOptions requestOptions) { + return detokenize(V1FlowDetokenizeRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse detokenize(V1FlowDetokenizeRequest request) { return detokenize(request, null); } public ApiClientHttpResponse detokenize( V1FlowDetokenizeRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/detokenize") - .build(); + .addPathSegments("v2/tokens/detokenize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -307,7 +401,7 @@ public ApiClientHttpResponse detokenize( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -317,19 +411,28 @@ public ApiClientHttpResponse detokenize( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowDetokenizeResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowDetokenizeResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -339,16 +442,24 @@ public ApiClientHttpResponse tokenize() { return tokenize(V1FlowTokenizeRequest.builder().build()); } + public ApiClientHttpResponse tokenize(RequestOptions requestOptions) { + return tokenize(V1FlowTokenizeRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse tokenize(V1FlowTokenizeRequest request) { return tokenize(request, null); } public ApiClientHttpResponse tokenize( V1FlowTokenizeRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/tokens/tokenize") - .build(); + .addPathSegments("v2/tokens/tokenize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -357,7 +468,7 @@ public ApiClientHttpResponse tokenize( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -367,19 +478,28 @@ public ApiClientHttpResponse tokenize( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowTokenizeResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowTokenizeResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } @@ -389,16 +509,24 @@ public ApiClientHttpResponse flowvaultmetrics() { return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build()); } + public ApiClientHttpResponse flowvaultmetrics(RequestOptions requestOptions) { + return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build(), requestOptions); + } + public ApiClientHttpResponse flowvaultmetrics(V1FlowVaultMetricsRequest request) { return flowvaultmetrics(request, null); } public ApiClientHttpResponse flowvaultmetrics( V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/vaults/metrics") - .build(); + .addPathSegments("v2/vaults/metrics"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -407,7 +535,7 @@ public ApiClientHttpResponse flowvaultmetrics( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -417,19 +545,28 @@ public ApiClientHttpResponse flowvaultmetrics( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowVaultMetricsResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1FlowVaultMetricsResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java index f09397b0..1fd8d151 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java @@ -191,5 +191,15 @@ public Builder uniqueValues(List uniqueValues) { public V1DeleteRequest build() { return new V1DeleteRequest(vaultId, tableName, skyflowIDs, uniqueValues, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java index b2fc71f5..d37e9434 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java @@ -127,5 +127,15 @@ public Builder tokens(List tokens) { public V1FlowDeleteTokenRequest build() { return new V1FlowDeleteTokenRequest(vaultId, tokens, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java index 78efb3fc..1d9105ff 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java @@ -161,5 +161,15 @@ public Builder tokenGroupRedactions(List tokenGroupRedac public V1FlowDetokenizeRequest build() { return new V1FlowDetokenizeRequest(vaultId, tokens, tokenGroupRedactions, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java index 9144e16d..4a843b10 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java @@ -130,5 +130,15 @@ public Builder data(List data) { public V1FlowTokenizeRequest build() { return new V1FlowTokenizeRequest(vaultId, data, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java index 8ba19a46..6c2e508c 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java @@ -97,5 +97,15 @@ public Builder vaultId(String vaultId) { public V1FlowVaultMetricsRequest build() { return new V1FlowVaultMetricsRequest(vaultId, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java index cfa0951c..c1b9cac1 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java @@ -362,5 +362,15 @@ public V1GetRequest build() { records, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java index 21bffc3a..76bce6e1 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java @@ -186,5 +186,15 @@ public Builder upsert(V1Upsert upsert) { public V1InsertRequest build() { return new V1InsertRequest(vaultId, tableName, records, upsert, 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/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java index e45f4751..fa8621c7 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java @@ -186,5 +186,15 @@ public Builder updateType(FlowEnumUpdateType updateType) { public V1UpdateRequest build() { return new V1UpdateRequest(vaultId, tableName, records, updateType, 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/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java index d1d76b52..077622b5 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java @@ -11,6 +11,7 @@ import com.skyflow.generated.rest.core.MediaTypes; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import java.io.IOException; @@ -40,6 +41,14 @@ public CompletableFuture> flowServ return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build()); } + /** + * Executes a query on the specified vault. + */ + public CompletableFuture> flowServiceExecuteQuery( + RequestOptions requestOptions) { + return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build(), requestOptions); + } + /** * Executes a query on the specified vault. */ @@ -53,10 +62,14 @@ public CompletableFuture> flowServ */ public CompletableFuture> flowServiceExecuteQuery( V1ExecuteQueryRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/query") - .build(); + .addPathSegments("v2/query"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -65,7 +78,7 @@ public CompletableFuture> flowServ throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -75,25 +88,34 @@ public CompletableFuture> flowServ 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue( - responseBody.string(), V1ExecuteQueryResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1ExecuteQueryResponse.class), response)); return; } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); future.completeExceptionally(new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response)); + "Error with status code " + response.code(), response.code(), errorBody, response)); return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new ApiClientException("Failed to deserialize response: " + e.getMessage(), e)); } catch (IOException e) { future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java index 2c1744a0..8f73440a 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java @@ -33,6 +33,13 @@ public CompletableFuture flowServiceExecuteQuery() { return this.rawClient.flowServiceExecuteQuery().thenApply(response -> response.body()); } + /** + * Executes a query on the specified vault. + */ + public CompletableFuture flowServiceExecuteQuery(RequestOptions requestOptions) { + return this.rawClient.flowServiceExecuteQuery(requestOptions).thenApply(response -> response.body()); + } + /** * Executes a query on the specified vault. */ diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java index a2bd4af2..1f4718d4 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java @@ -11,6 +11,7 @@ import com.skyflow.generated.rest.core.MediaTypes; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import java.io.IOException; @@ -36,6 +37,13 @@ public ApiClientHttpResponse flowServiceExecuteQuery() { return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build()); } + /** + * Executes a query on the specified vault. + */ + public ApiClientHttpResponse flowServiceExecuteQuery(RequestOptions requestOptions) { + return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build(), requestOptions); + } + /** * Executes a query on the specified vault. */ @@ -48,10 +56,14 @@ public ApiClientHttpResponse flowServiceExecuteQuery(V1E */ public ApiClientHttpResponse flowServiceExecuteQuery( V1ExecuteQueryRequest request, RequestOptions requestOptions) { - HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) .newBuilder() - .addPathSegments("v2/query") - .build(); + .addPathSegments("v2/query"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } RequestBody body; try { body = RequestBody.create( @@ -60,7 +72,7 @@ public ApiClientHttpResponse flowServiceExecuteQuery( throw new ApiClientException("Failed to serialize request", e); } Request okhttpRequest = new Request.Builder() - .url(httpUrl) + .url(httpUrl.build()) .method("POST", body) .headers(Headers.of(clientOptions.headers(requestOptions))) .addHeader("Content-Type", "application/json") @@ -70,19 +82,28 @@ public ApiClientHttpResponse flowServiceExecuteQuery( 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 ApiClientHttpResponse<>( - ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1ExecuteQueryResponse.class), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, V1ExecuteQueryResponse.class), response); } - String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); throw new ApiClientApiException( - "Error with status code " + response.code(), - response.code(), - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), - response); + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to deserialize response: " + e.getMessage(), e); } catch (IOException e) { throw new ApiClientException("Network error executing HTTP request", e); } diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java b/v3/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java index 332c0e1b..21c4869a 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java @@ -32,6 +32,13 @@ public V1ExecuteQueryResponse flowServiceExecuteQuery() { return this.rawClient.flowServiceExecuteQuery().body(); } + /** + * Executes a query on the specified vault. + */ + public V1ExecuteQueryResponse flowServiceExecuteQuery(RequestOptions requestOptions) { + return this.rawClient.flowServiceExecuteQuery(requestOptions).body(); + } + /** * Executes a query on the specified vault. */ diff --git a/v3/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java b/v3/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java index 2c04f126..822745f2 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java +++ b/v3/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java @@ -126,5 +126,15 @@ public Builder query(String query) { public V1ExecuteQueryRequest build() { return new V1ExecuteQueryRequest(vaultId, query, 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/v3/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java b/v3/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java index 0695a72e..7b58ebfd 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java @@ -3,22 +3,81 @@ */ package com.skyflow.generated.rest.types; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -public enum FlowEnumUpdateType { - UPDATE("UPDATE"), +public final class FlowEnumUpdateType { + public static final FlowEnumUpdateType REPLACE = new FlowEnumUpdateType(Value.REPLACE, "REPLACE"); - REPLACE("REPLACE"); + public static final FlowEnumUpdateType UPDATE = new FlowEnumUpdateType(Value.UPDATE, "UPDATE"); - private final String value; + private final Value value; - FlowEnumUpdateType(String value) { + private final String string; + + FlowEnumUpdateType(Value value, String string) { this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; } - @JsonValue @java.lang.Override + @JsonValue public String toString() { - return this.value; + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) + || (other instanceof FlowEnumUpdateType && this.string.equals(((FlowEnumUpdateType) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case REPLACE: + return visitor.visitReplace(); + case UPDATE: + return visitor.visitUpdate(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static FlowEnumUpdateType valueOf(String value) { + switch (value) { + case "REPLACE": + return REPLACE; + case "UPDATE": + return UPDATE; + default: + return new FlowEnumUpdateType(Value.UNKNOWN, value); + } + } + + public enum Value { + UPDATE, + + REPLACE, + + UNKNOWN + } + + public interface Visitor { + T visitUpdate(); + + T visitReplace(); + + T visitUnknown(String unknownType); } } diff --git a/v3/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java b/v3/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java index 88084f6d..bea05d31 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java @@ -189,5 +189,15 @@ public Builder httpCode(Integer httpCode) { public FlowTokenizeResponseObjectToken build() { return new FlowTokenizeResponseObjectToken(tokenGroupName, token, error, httpCode, 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/v3/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java b/v3/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java index 3051270a..8f94c32d 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java @@ -147,5 +147,15 @@ public Builder type(String type) { public GoogleprotobufAny build() { return new GoogleprotobufAny(type, 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/v3/src/main/java/com/skyflow/generated/rest/types/ProtobufNullValue.java b/v3/src/main/java/com/skyflow/generated/rest/types/ProtobufNullValue.java new file mode 100644 index 00000000..f53b261e --- /dev/null +++ b/v3/src/main/java/com/skyflow/generated/rest/types/ProtobufNullValue.java @@ -0,0 +1,73 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class ProtobufNullValue { + public static final ProtobufNullValue NULL_VALUE = new ProtobufNullValue(Value.NULL_VALUE, "NULL_VALUE"); + + private final Value value; + + private final String string; + + ProtobufNullValue(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 ProtobufNullValue && this.string.equals(((ProtobufNullValue) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case NULL_VALUE: + return visitor.visitNullValue(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ProtobufNullValue valueOf(String value) { + switch (value) { + case "NULL_VALUE": + return NULL_VALUE; + default: + return new ProtobufNullValue(Value.UNKNOWN, value); + } + } + + public enum Value { + NULL_VALUE, + + UNKNOWN + } + + public interface Visitor { + T visitNullValue(); + + T visitUnknown(String unknownType); + } +} diff --git a/v3/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java b/v3/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java index b0fc6c44..7ece3524 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java @@ -140,5 +140,15 @@ public Builder details(List details) { public RpcStatus build() { return new RpcStatus(code, message, details, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java index e0133640..60a0fdf9 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java @@ -126,5 +126,15 @@ public Builder redaction(String redaction) { public V1ColumnRedactions build() { return new V1ColumnRedactions(columnName, redaction, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java index b20fd804..9d9bcf34 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java @@ -98,5 +98,15 @@ public Builder records(List records) { public V1DeleteResponse build() { return new V1DeleteResponse(records, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java index 9d71a0ad..d4e294ce 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java @@ -157,5 +157,15 @@ public Builder httpCode(Integer httpCode) { public V1DeleteResponseObject build() { return new V1DeleteResponseObject(skyflowId, error, httpCode, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java index 5f866d0d..1cbcf1d7 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java @@ -157,5 +157,15 @@ public Builder httpCode(Integer httpCode) { public V1DeleteTokenResponseObject build() { return new V1DeleteTokenResponseObject(value, error, httpCode, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java index b485d0ec..e8e2ed88 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java @@ -97,5 +97,15 @@ public Builder data(Map data) { public V1ExecuteQueryRecordResponse build() { return new V1ExecuteQueryRecordResponse(data, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java index 2b45980a..91f55f86 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java @@ -123,5 +123,15 @@ public Builder metadata(V1ExecuteQueryResponseMetadata metadata) { public V1ExecuteQueryResponse build() { return new V1ExecuteQueryResponse(records, metadata, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java index e68fd1c8..298ce17a 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java @@ -98,5 +98,15 @@ public Builder columns(List columns) { public V1ExecuteQueryResponseMetadata build() { return new V1ExecuteQueryResponseMetadata(columns, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java index 85ee0edd..77014f24 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java @@ -99,5 +99,15 @@ public Builder tokens(List tokens) { public V1FlowDeleteTokenResponse build() { return new V1FlowDeleteTokenResponse(tokens, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java index 81bd650c..3702929c 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java @@ -99,5 +99,15 @@ public Builder response(List response) { public V1FlowDetokenizeResponse build() { return new V1FlowDetokenizeResponse(response, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java index 6c936147..07ff4d96 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java @@ -250,5 +250,15 @@ public V1FlowDetokenizeResponseObject build() { return new V1FlowDetokenizeResponseObject( token, value, tokenGroupName, error, httpCode, metadata, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java index effb6900..4a285b3f 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java @@ -158,5 +158,15 @@ public Builder token(Object token) { public V1FlowTokenizeRequestObject build() { return new V1FlowTokenizeRequestObject(value, tokenGroupNames, token, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java index 1cc36e06..34a9122e 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java @@ -99,5 +99,15 @@ public Builder response(List response) { public V1FlowTokenizeResponse build() { return new V1FlowTokenizeResponse(response, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java index 51fbc049..332b2542 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java @@ -129,5 +129,15 @@ public Builder tokens(List tokens) { public V1FlowTokenizeResponseObject build() { return new V1FlowTokenizeResponseObject(value, tokens, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java index ef84dd03..73286be6 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java @@ -97,5 +97,15 @@ public Builder tables(Map tables) { public V1FlowVaultMetricsData build() { return new V1FlowVaultMetricsData(tables, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java index cef9425e..1d8cec3c 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java @@ -122,5 +122,15 @@ public Builder error(Map error) { public V1FlowVaultMetricsResponse build() { return new V1FlowVaultMetricsResponse(data, error, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java index 9037e23e..ed1dacb8 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java @@ -221,5 +221,15 @@ public V1GetRequestData build() { return new V1GetRequestData( tableName, skyflowIDs, columnRedactions, columns, uniqueValues, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java index 2b92160f..83f17cda 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java @@ -98,5 +98,15 @@ public Builder records(List records) { public V1GetResponse build() { return new V1GetResponse(records, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java index 07a5eedc..b3a6f265 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java @@ -183,5 +183,15 @@ public Builder upsert(V1Upsert upsert) { public V1InsertRecordData build() { return new V1InsertRecordData(data, tokens, tableName, upsert, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java index 83be00b4..09aa1c6d 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java @@ -98,5 +98,15 @@ public Builder records(List records) { public V1InsertResponse build() { return new V1InsertResponse(records, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java index 2e1b0296..91e61b9b 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java @@ -281,5 +281,15 @@ public V1RecordResponseObject build() { return new V1RecordResponseObject( skyflowId, tokens, data, hashedData, error, httpCode, tableName, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java index c969df86..8e488da8 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java @@ -126,5 +126,15 @@ public Builder redaction(String redaction) { public V1TokenGroupRedactions build() { return new V1TokenGroupRedactions(tokenGroupName, redaction, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java index 5238c2d7..efa2320d 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java @@ -97,5 +97,15 @@ public Builder data(Map data) { public V1UniqueValue build() { return new V1UniqueValue(data, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java index 52d98a44..5698025e 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java @@ -189,5 +189,15 @@ public Builder tableName(String tableName) { public V1UpdateRecordData build() { return new V1UpdateRecordData(skyflowId, data, tokens, tableName, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java index 37c91744..0873938a 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java @@ -98,5 +98,15 @@ public Builder records(List records) { public V1UpdateResponse build() { return new V1UpdateResponse(records, 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/v3/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java b/v3/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java index 387c80c4..1b6edcec 100644 --- a/v3/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java +++ b/v3/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java @@ -123,5 +123,15 @@ public Builder uniqueColumns(List uniqueColumns) { public V1Upsert build() { return new V1Upsert(updateType, uniqueColumns, 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/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java new file mode 100644 index 00000000..145bc8d1 --- /dev/null +++ b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java @@ -0,0 +1,72 @@ +package com.skyflow; + +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.vault.controller.VaultController; +import java.lang.reflect.Field; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies the client-wide HTTP config setters on {@code Skyflow.builder()} store their values and + * propagate to the vault controller(s) — both when set before {@code addVaultConfig} and after + * (which exercises the propagation loop over already-created controllers). + */ +public class SkyflowClientBuilderHttpConfigTests { + + private VaultConfig vaultConfig() { + VaultConfig cfg = new VaultConfig(); + cfg.setVaultId("test_vault_id"); + cfg.setClusterId("test_cluster_id"); + cfg.setEnv(Env.PROD); + return cfg; + } + + private Object commonField(VaultController controller, String name) throws Exception { + Field field = VaultClient.class.getDeclaredField(name); // fields declared on VaultClient superclass + field.setAccessible(true); + return field.get(controller); + } + + @Test + public void clientWideConfigSetBeforeAddVaultReachesController() throws Exception { + Skyflow client = Skyflow.builder() + .timeout(30) + .maxRetries(2) + .initialRetryDelay(300L) + .maxRetryDelay(1500L) + .addVaultConfig(vaultConfig()) + .build(); + + VaultController controller = client.vault(); + Assert.assertEquals(Integer.valueOf(30), commonField(controller, "commonTimeout")); + Assert.assertEquals(Integer.valueOf(2), commonField(controller, "commonMaxRetries")); + Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelay")); + Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelay")); + } + + @Test + public void clientWideConfigSetAfterAddVaultPropagatesToExistingController() throws Exception { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(vaultConfig()); + // Set config AFTER the controller already exists -> exercises propagateHttpConfig's loop. + builder.timeout(45).maxRetries(4).initialRetryDelay(700L).maxRetryDelay(3000L); + Skyflow client = builder.build(); + + VaultController controller = client.vault(); + Assert.assertEquals(Integer.valueOf(45), commonField(controller, "commonTimeout")); + Assert.assertEquals(Integer.valueOf(4), commonField(controller, "commonMaxRetries")); + Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelay")); + Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelay")); + } + + @Test + public void noClientWideConfigLeavesCommonFieldsNull() throws Exception { + Skyflow client = Skyflow.builder().addVaultConfig(vaultConfig()).build(); + + VaultController controller = client.vault(); + Assert.assertNull(commonField(controller, "commonTimeout")); + Assert.assertNull(commonField(controller, "commonMaxRetries")); + Assert.assertNull(commonField(controller, "commonInitialRetryDelay")); + Assert.assertNull(commonField(controller, "commonMaxRetryDelay")); + } +} diff --git a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java new file mode 100644 index 00000000..b8c1933e --- /dev/null +++ b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java @@ -0,0 +1,171 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.core.RetryInterceptor; +import java.lang.reflect.Field; +import java.time.Duration; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies the HTTP timeout/retry config is wired onto the shared OkHttpClient and that the + * timeout precedence (vault-level > client-wide > SDK default) resolves correctly. Uses an API-key + * credential so {@code setBearerToken()} builds the client without a network call. + */ +public class VaultClientHttpConfigTests { + + private static final int DEFAULT_CALL_TIMEOUT_MS = 60000; // SDK default = 60s + + private VaultConfig apiKeyConfig() { + VaultConfig cfg = new VaultConfig(); + cfg.setVaultId("vault123"); + cfg.setClusterId("cluster123"); + cfg.setEnv(Env.PROD); + Credentials credentials = new Credentials(); + credentials.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + cfg.setCredentials(credentials); + return cfg; + } + + private OkHttpClient sharedClient(VaultClient client) throws Exception { + Field field = VaultClient.class.getDeclaredField("sharedHttpClient"); + field.setAccessible(true); + return (OkHttpClient) field.get(client); + } + + @Test + public void defaultTimeoutAppliedWhenNothingConfigured() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + Assert.assertEquals(DEFAULT_CALL_TIMEOUT_MS, sharedClient(client).callTimeoutMillis()); + } + + @Test + public void vaultLevelTimeoutOverridesDefault() throws Exception { + VaultConfig cfg = apiKeyConfig(); + cfg.setTimeout(15); // seconds + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + Assert.assertEquals(15000, sharedClient(client).callTimeoutMillis()); + } + + @Test + public void clientLevelTimeoutUsedWhenNoVaultOverride() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setCommonHttpConfig(45, null, null, null); // client-wide 45s, no vault override + client.setBearerToken(); + + Assert.assertEquals(45000, sharedClient(client).callTimeoutMillis()); + } + + @Test + public void vaultLevelTimeoutOverridesClientLevel() throws Exception { + VaultConfig cfg = apiKeyConfig(); + cfg.setTimeout(15); // vault-level + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setCommonHttpConfig(45, null, null, null); // client-wide 45s — should lose to vault's 15s + client.setBearerToken(); + + Assert.assertEquals(15000, sharedClient(client).callTimeoutMillis()); + } + + @Test + public void callTimeoutIsBoundedNotZero() throws Exception { + // Regression guard: the real bug was callTimeout == 0 (unbounded). It must never be 0 by default. + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + Assert.assertTrue(sharedClient(client).callTimeoutMillis() > 0); + } + + @Test + public void vaultConfigStoresHttpFieldsAndDefaultsToNull() throws SkyflowException { + VaultConfig cfg = new VaultConfig(); + Assert.assertNull(cfg.getTimeout()); + Assert.assertNull(cfg.getMaxRetries()); + Assert.assertNull(cfg.getInitialRetryDelay()); + Assert.assertNull(cfg.getMaxRetryDelay()); + + cfg.setTimeout(30); + cfg.setMaxRetries(2); + cfg.setInitialRetryDelay(250L); + cfg.setMaxRetryDelay(1500L); + + Assert.assertEquals(Integer.valueOf(30), cfg.getTimeout()); + Assert.assertEquals(Integer.valueOf(2), cfg.getMaxRetries()); + Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelay()); + Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelay()); + } + + private RetryInterceptor retryInterceptor(VaultClient client) throws Exception { + for (Interceptor interceptor : sharedClient(client).interceptors()) { + if (interceptor instanceof RetryInterceptor) { + return (RetryInterceptor) interceptor; + } + } + throw new AssertionError("Fern RetryInterceptor was not attached to the shared client"); + } + + private int intField(Object obj, String name) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.getInt(obj); + } + + // Fern stores the delays as java.time.Duration; compare in millis. + private long durationFieldMillis(Object obj, String name) throws Exception { + Field field = obj.getClass().getDeclaredField(name); + field.setAccessible(true); + return ((Duration) field.get(obj)).toMillis(); + } + + @Test + public void defaultRetryConfigAppliedToInterceptor() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + RetryInterceptor ri = retryInterceptor(client); + Assert.assertEquals(0, intField(ri, "maxRetries")); // SDK default: retries off (opt-in) + Assert.assertEquals(500L, durationFieldMillis(ri, "initialRetryDelay")); + Assert.assertEquals(2000L, durationFieldMillis(ri, "maxRetryDelay")); + } + + @Test + public void vaultLevelRetryConfigOverridesDefault() throws Exception { + VaultConfig cfg = apiKeyConfig(); + cfg.setMaxRetries(1); + cfg.setInitialRetryDelay(250L); + cfg.setMaxRetryDelay(1200L); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + RetryInterceptor ri = retryInterceptor(client); + Assert.assertEquals(1, intField(ri, "maxRetries")); + Assert.assertEquals(250L, durationFieldMillis(ri, "initialRetryDelay")); + Assert.assertEquals(1200L, durationFieldMillis(ri, "maxRetryDelay")); + } + + @Test + public void clientLevelRetryConfigUsedWhenNoVaultOverride() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setCommonHttpConfig(null, 2, 300L, 1500L); // client-wide retry config, no vault override + client.setBearerToken(); + + RetryInterceptor ri = retryInterceptor(client); + Assert.assertEquals(2, intField(ri, "maxRetries")); + Assert.assertEquals(300L, durationFieldMillis(ri, "initialRetryDelay")); + Assert.assertEquals(1500L, durationFieldMillis(ri, "maxRetryDelay")); + } +} diff --git a/v3/src/test/java/com/skyflow/VaultClientTests.java b/v3/src/test/java/com/skyflow/VaultClientTests.java index e9b5a65f..a507b438 100644 --- a/v3/src/test/java/com/skyflow/VaultClientTests.java +++ b/v3/src/test/java/com/skyflow/VaultClientTests.java @@ -232,7 +232,7 @@ public void testUpsertAtRequestLevel() { V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); Assert.assertNotNull(result.getUpsert()); Assert.assertEquals("col1", result.getUpsert().get().getUniqueColumns().get().get(0)); - Assert.assertEquals("REPLACE", result.getUpsert().get().getUpdateType().get().name()); + Assert.assertEquals("REPLACE", result.getUpsert().get().getUpdateType().get().getEnumValue().name()); } @Test @@ -252,7 +252,7 @@ public void testUpsertAtRecordLevel() { V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); Assert.assertNotNull(result.getRecords().get().get(0).getUpsert()); Assert.assertEquals("col2", result.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get().get(0)); - Assert.assertEquals("UPDATE", result.getRecords().get().get(0).getUpsert().get().getUpdateType().get().name()); + Assert.assertEquals("UPDATE", result.getRecords().get().get(0).getUpsert().get().getUpdateType().get().getEnumValue().name()); } @Test @@ -344,8 +344,8 @@ public void testUpsertTypeAtBothRequestAndRecordLevelSerializesBoth() { .build(); V1InsertRequest result = vaultClient.getBulkInsertRequestBody(request, vaultConfig); - Assert.assertEquals("REPLACE", result.getUpsert().get().getUpdateType().get().name()); - Assert.assertEquals("UPDATE", result.getRecords().get().get(0).getUpsert().get().getUpdateType().get().name()); + Assert.assertEquals("REPLACE", result.getUpsert().get().getUpdateType().get().getEnumValue().name()); + Assert.assertEquals("UPDATE", result.getRecords().get().get(0).getUpsert().get().getUpdateType().get().getEnumValue().name()); } @Test From 85fcac95be4528fb778fac1fef032eba043750df Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Tue, 21 Jul 2026 14:43:31 +0000 Subject: [PATCH 4/6] [AUTOMATED] Private Release 2.1.1-dev-1236334 --- v3/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/pom.xml b/v3/pom.xml index d27fa575..c529d8bf 100644 --- a/v3/pom.xml +++ b/v3/pom.xml @@ -11,7 +11,7 @@ skyflow-java - 2.1.1-dev.ce79ce1 + 2.1.1-dev.1236334 jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language From 31964518b2764e26db4b368ab2dc814c72192861 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 22 Jul 2026 17:59:08 +0530 Subject: [PATCH 5/6] SK-3002 retry backoff support (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SK-3002 add SkyflowRetryInterceptor (configurable retry count + backoff) Hand-written OkHttp interceptor replicating Fern's retry algorithm (exponential backoff + proportional jitter, factor 0.2 fixed). Exposes maxRetries / initialRetryDelayMillis / maxRetryDelayMillis; retries on 408/429/5xx. Carries a TODO to replace with Fern's generated interceptor once a version exposing the delay knobs is vendored. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 expose configurable timeout + retries (client & vault level) - VaultConfig: vault-level timeout/maxRetries/initialRetryDelayMillis/ maxRetryDelayMillis (nullable = inherit) - Skyflow.SkyflowClientBuilder: flat client-wide setters + propagation to existing controllers (mirrors addSkyflowCredentials) - VaultClient.updateExecutorInHTTP: set callTimeout + attach SkyflowRetryInterceptor; per-field precedence vault > client > SDK default (defaults 60s / 3 / 500ms / 2000ms) Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 tests for configurable timeout + retries - SkyflowRetryInterceptorTests: retry count, stop-on-success, 408/429/5xx triggers, non-retryable passthrough, zero-retries - VaultClientHttpConfigTests: callTimeout applied to shared client, precedence (vault > client > default), bounded-not-zero regression guard, VaultConfig field getters/setters Note: pre-existing VaultClientTests#testSetBearerTokenWithEnvCredentials fails on Java 21 (PowerMock byte-buddy cannot instrument JDK security classes) — reproduced on base v3, unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 add sample for configurable timeout + retries Demonstrates client-wide defaults on Skyflow.builder() and per-vault overrides on VaultConfig, with precedence and unit notes. Compiles against the local SDK build (samples module pins a released version, so it builds once this change ships). Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 add MockWebServer integration test (live timeout + retry proof) Drives an OkHttpClient built like VaultClient (callTimeout + SkyflowRetry Interceptor) against a real loopback MockWebServer: - retries 503 then succeeds (server sees 1+retries requests) - exhausts retries, returns last failure - no retry on 400 - callTimeout aborts a never-responding server (~1s, not hanging) -- the fix - observable backoff between retries Adds com.squareup.okhttp3:mockwebserver as a test dependency. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 remove MockWebServer test (breaks CI on PowerMock + Java 21) MockWebServer's static init loads JDK security providers, which the PowerMock/byte-buddy agent fails to instrument on Java 21 (IllegalClassFormatException: Unsupported class file major version 65) -> ExceptionInInitializerError in setUp. This is the same agent/JDK incompatibility that already fails testSetBearerTokenWithEnvCredentials. Retry/timeout behavior remains covered by the mocked-Chain unit tests (SkyflowRetryInterceptorTests) and the config/precedence tests (VaultClientHttpConfigTests), both green in CI. Also drops the mockwebserver test dependency. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 raise coverage for new timeout/retry code Cover the previously-untested new code paths flagged by Codecov: - Skyflow.builder() setters + propagateHttpConfig loop (config set before and after addVaultConfig) -> SkyflowClientBuilderHttpConfigTests - resolveInt/resolveLong vault + client branches via maxRetries + backoff precedence, asserted on the built SkyflowRetryInterceptor's fields - interceptor InterruptedException-during-backoff path (throws IOException, preserves interrupt flag) Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002-retry-backoff-support * SK-3002 use Fern v4 generated RetryInterceptor; rename knobs to initialRetryDelay/maxRetryDelay - Swap hand-written SkyflowRetryInterceptor for Fern's v4 generated RetryInterceptor (public 4-arg ctor: maxRetries, Optional initial/max delay, Optional jitter); delete SkyflowRetryInterceptor + its unit test - Rename public knobs initialRetryDelayMillis/maxRetryDelayMillis -> initialRetryDelay/ maxRetryDelay (Fern naming); still long ms, client + vault levels only - VaultClient attaches the generated RetryInterceptor on the shared client, passing resolved values as Optional (jitter Optional.empty -> Fern default 0.2). Gains Retry-After / X-RateLimit-Reset header awareness for free - Update VaultClientHttpConfigTests to reflect Fern's Duration fields; rename sample Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 fix CI coverage: prepend @{argLine} so JaCoCo agent attaches The v4-regen added a static (--add-opens ...) to maven-surefire-plugin, which overwrote the argLine JaCoCo's prepare-agent injects. The coverage agent therefore never attached -> no jacoco.exec -> "Skipping JaCoCo ... missing execution data" -> no jacoco.xml -> Codecov "No coverage reports found" (build green, 439 tests pass, but upload fails). Prepend @{argLine} (Surefire late-binding of JaCoCo's property) so both the coverage agent and the --add-opens flags apply. Verified locally: jacoco.exec and jacoco.xml are generated again. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 default maxRetries=0 (retries opt-in) to avoid retrying non-idempotent writes Retries fire on 408/429/5xx for all operations regardless of HTTP verb, so auto-retrying a non-idempotent write (e.g. insert without upsert) after a 5xx/timeout could create a duplicate record. Default maxRetries to 0 so the unconfigured SDK does not retry; customers opt in by setting maxRetries. The 60s callTimeout still bounds every call. initialRetryDelay/maxRetryDelay defaults only take effect once maxRetries > 0. Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 rename retry delay knobs to Fern's public names: initialRetryDelayMillis / maxRetryDelayMillis Match Fern's public setter names exactly (initialRetryDelayMillis / maxRetryDelayMillis) so the unit (ms) is self-documenting and consistent with Fern. timeout stays in seconds (also matching Fern). Removes the mixed-unit footgun where the dropped `Millis` suffix made `initialRetryDelay(500)` look like seconds. Renamed across VaultConfig, Skyflow.SkyflowClientBuilder, VaultClient, the config tests, and the sample. Fern interceptor field reflection in tests kept as-is (Fern's field is `initialRetryDelay: Duration`, not the ctor param name). Co-Authored-By: Claude Opus 4.8 (1M context) * SK-3002 classify client-side timeout as 408 in batch error records Timeouts previously surfaced in response.errors as code 500 with a raw "...ApiClientException: Network error..." message, indistinguishable from a server error. In the four bulk handlers, when a batch fails with a non-API exception, walk the cause chain for an InterruptedIOException (OkHttp's call/socket timeout) and report 408 + "Request timed out." instead of 500. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: skyflow-bharti --- .../vault/TimeoutAndRetryConfigExample.java | 12 ++-- v3/src/main/java/com/skyflow/Skyflow.java | 16 ++--- v3/src/main/java/com/skyflow/VaultClient.java | 20 +++--- .../java/com/skyflow/config/VaultConfig.java | 24 +++---- v3/src/main/java/com/skyflow/utils/Utils.java | 33 ++++++--- .../SkyflowClientBuilderHttpConfigTests.java | 18 ++--- .../skyflow/VaultClientHttpConfigTests.java | 16 ++--- .../java/com/skyflow/utils/UtilsTests.java | 68 +++++++++++++++++++ 8 files changed, 146 insertions(+), 61 deletions(-) diff --git a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java index d537df30..eb4bf1e9 100644 --- a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java +++ b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java @@ -16,9 +16,9 @@ * request including retries and backoff). Default: 60. *

  • {@code maxRetries} – retry attempts after the first failure (retries on HTTP * 408 / 429 / 5xx). Default: 0 — retries are OFF unless you set this (avoids auto-retrying non-idempotent writes).
  • - *
  • {@code initialRetryDelay} – base backoff before the first retry, in milliseconds. + *
  • {@code initialRetryDelayMillis} – base backoff before the first retry, in milliseconds. * Default: 500.
  • - *
  • {@code maxRetryDelay} – cap on the (exponentially growing) backoff, in + *
  • {@code maxRetryDelayMillis} – cap on the (exponentially growing) backoff, in * milliseconds. Default: 2000.
  • * * @@ -45,8 +45,8 @@ public static void main(String[] args) { // and then the SDK default. vaultConfig.setTimeout(30); // seconds – tighter overall ceiling for this vault vaultConfig.setMaxRetries(2); // fewer retries for this vault - vaultConfig.setInitialRetryDelay(500L); - vaultConfig.setMaxRetryDelay(1000L); + vaultConfig.setInitialRetryDelayMillis(500L); + vaultConfig.setMaxRetryDelayMillis(1000L); // Step 3: Create the Skyflow client. Client-wide defaults apply to every vault // unless that vault overrides them (as above). @@ -54,8 +54,8 @@ public static void main(String[] args) { .setLogLevel(LogLevel.ERROR) .timeout(60) // seconds – client-wide overall call timeout .maxRetries(3) // client-wide retry attempts - .initialRetryDelay(500L) // client-wide base backoff (ms) - .maxRetryDelay(2000L) // client-wide backoff cap (ms) + .initialRetryDelayMillis(500L) // client-wide base backoff (ms) + .maxRetryDelayMillis(2000L) // client-wide backoff cap (ms) .addVaultConfig(vaultConfig) .build(); diff --git a/v3/src/main/java/com/skyflow/Skyflow.java b/v3/src/main/java/com/skyflow/Skyflow.java index 81d3eb5f..df530215 100644 --- a/v3/src/main/java/com/skyflow/Skyflow.java +++ b/v3/src/main/java/com/skyflow/Skyflow.java @@ -58,8 +58,8 @@ public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder // Client-wide HTTP config defaults (apply to all vaults unless a vault overrides). null => SDK default. private Integer timeout; private Integer maxRetries; - private Long initialRetryDelay; - private Long maxRetryDelay; + private Long initialRetryDelayMillis; + private Long maxRetryDelayMillis; public SkyflowClientBuilder() { this.vaultConfigMap = new LinkedHashMap<>(); @@ -84,7 +84,7 @@ public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws Skyfl } else { this.vaultConfigMap.put(vaultConfigCopy.getVaultId(), vaultConfigCopy); // add new config in map VaultController controller = new VaultController(vaultConfigCopy, this.skyflowCredentials); // new controller with new config - controller.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelay, this.maxRetryDelay); + controller.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis); this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), controller); LogUtil.printInfoLog(Utils.parameterizedString( InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfigCopy.getVaultId())); @@ -122,22 +122,22 @@ public SkyflowClientBuilder maxRetries(int maxRetries) { } /** Client-wide base retry backoff in milliseconds. Applies to all vaults unless a vault overrides it. */ - public SkyflowClientBuilder initialRetryDelay(long initialRetryDelay) { - this.initialRetryDelay = initialRetryDelay; + public SkyflowClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = initialRetryDelayMillis; propagateHttpConfig(); return this; } /** Client-wide retry backoff cap in milliseconds. Applies to all vaults unless a vault overrides it. */ - public SkyflowClientBuilder maxRetryDelay(long maxRetryDelay) { - this.maxRetryDelay = maxRetryDelay; + public SkyflowClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = maxRetryDelayMillis; propagateHttpConfig(); return this; } private void propagateHttpConfig() { for (VaultController vault : this.vaultClientsMap.values()) { - vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelay, this.maxRetryDelay); + vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis); } } diff --git a/v3/src/main/java/com/skyflow/VaultClient.java b/v3/src/main/java/com/skyflow/VaultClient.java index 2540bb70..b06f2bf6 100644 --- a/v3/src/main/java/com/skyflow/VaultClient.java +++ b/v3/src/main/java/com/skyflow/VaultClient.java @@ -50,8 +50,8 @@ public class VaultClient { // Client-wide (Skyflow builder) HTTP config defaults; null => fall back to the SDK defaults below. private Integer commonTimeout; private Integer commonMaxRetries; - private Long commonInitialRetryDelay; - private Long commonMaxRetryDelay; + private Long commonInitialRetryDelayMillis; + private Long commonMaxRetryDelayMillis; // SDK defaults, used when neither the vault-level nor the client-wide value is set. private static final int DEFAULT_TIMEOUT_SECONDS = 60; private static final int DEFAULT_MAX_RETRIES = 0; // retries OFF by default (opt-in) so non-idempotent writes aren't auto-retried @@ -85,11 +85,11 @@ protected void setCommonCredentials(Credentials commonCredentials) throws Skyflo * so the next call rebuilds with the new values. */ protected void setCommonHttpConfig(Integer timeout, Integer maxRetries, - Long initialRetryDelay, Long maxRetryDelay) { + Long initialRetryDelayMillis, Long maxRetryDelayMillis) { this.commonTimeout = timeout; this.commonMaxRetries = maxRetries; - this.commonInitialRetryDelay = initialRetryDelay; - this.commonMaxRetryDelay = maxRetryDelay; + this.commonInitialRetryDelayMillis = initialRetryDelayMillis; + this.commonMaxRetryDelayMillis = maxRetryDelayMillis; this.sharedHttpClient = null; this.apiClient = null; } @@ -171,16 +171,16 @@ protected void updateExecutorInHTTP() { if (sharedHttpClient == null) { int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS); int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES); - long initialRetryDelay = resolveLong( - vaultConfig.getInitialRetryDelay(), commonInitialRetryDelay, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); - long maxRetryDelay = resolveLong( - vaultConfig.getMaxRetryDelay(), commonMaxRetryDelay, DEFAULT_MAX_RETRY_DELAY_MILLIS); + long initialRetryDelayMillis = resolveLong( + vaultConfig.getInitialRetryDelayMillis(), commonInitialRetryDelayMillis, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); + long maxRetryDelayMillis = resolveLong( + vaultConfig.getMaxRetryDelayMillis(), commonMaxRetryDelayMillis, DEFAULT_MAX_RETRY_DELAY_MILLIS); sharedHttpClient = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) .callTimeout(timeoutSeconds, TimeUnit.SECONDS) // overall ceiling; bounds the whole call incl. retries .addInterceptor(new RetryInterceptor( // OUTER: retries (Fern generated; jitter default 0.2) - maxRetries, Optional.of(initialRetryDelay), Optional.of(maxRetryDelay), Optional.empty())) + maxRetries, Optional.of(initialRetryDelayMillis), Optional.of(maxRetryDelayMillis), Optional.empty())) .addInterceptor(chain -> { // INNER: auth (reads this.token per request) Request requestWithAuth = chain.request().newBuilder() .header("Authorization", "Bearer " + this.token) diff --git a/v3/src/main/java/com/skyflow/config/VaultConfig.java b/v3/src/main/java/com/skyflow/config/VaultConfig.java index 69bec4d0..29c7f02c 100644 --- a/v3/src/main/java/com/skyflow/config/VaultConfig.java +++ b/v3/src/main/java/com/skyflow/config/VaultConfig.java @@ -11,8 +11,8 @@ public class VaultConfig implements Cloneable { // HTTP timeout & retry config (vault-level overrides). null => inherit client-wide default, then SDK default. private Integer timeout; // overall call timeout, in seconds private Integer maxRetries; // retry attempts after the first failure - private Long initialRetryDelay; // base backoff before the first retry, in ms - private Long maxRetryDelay; // cap on the (exponentially growing) backoff, in ms + private Long initialRetryDelayMillis; // base backoff before the first retry, in ms + private Long maxRetryDelayMillis; // cap on the (exponentially growing) backoff, in ms public VaultConfig() { this.vaultId = null; @@ -22,8 +22,8 @@ public VaultConfig() { this.credentials = null; this.timeout = null; this.maxRetries = null; - this.initialRetryDelay = null; - this.maxRetryDelay = null; + this.initialRetryDelayMillis = null; + this.maxRetryDelayMillis = null; } public String getVaultId() { @@ -84,22 +84,22 @@ public void setMaxRetries(Integer maxRetries) { this.maxRetries = maxRetries; } - public Long getInitialRetryDelay() { - return initialRetryDelay; + public Long getInitialRetryDelayMillis() { + return initialRetryDelayMillis; } /** Base retry backoff in milliseconds for this vault. Overrides the client-wide default. */ - public void setInitialRetryDelay(Long initialRetryDelay) { - this.initialRetryDelay = initialRetryDelay; + public void setInitialRetryDelayMillis(Long initialRetryDelayMillis) { + this.initialRetryDelayMillis = initialRetryDelayMillis; } - public Long getMaxRetryDelay() { - return maxRetryDelay; + public Long getMaxRetryDelayMillis() { + return maxRetryDelayMillis; } /** Cap on retry backoff in milliseconds for this vault. Overrides the client-wide default. */ - public void setMaxRetryDelay(Long maxRetryDelay) { - this.maxRetryDelay = maxRetryDelay; + public void setMaxRetryDelayMillis(Long maxRetryDelayMillis) { + this.maxRetryDelayMillis = maxRetryDelayMillis; } @Override diff --git a/v3/src/main/java/com/skyflow/utils/Utils.java b/v3/src/main/java/com/skyflow/utils/Utils.java index cdd0b03b..0eb6d656 100644 --- a/v3/src/main/java/com/skyflow/utils/Utils.java +++ b/v3/src/main/java/com/skyflow/utils/Utils.java @@ -24,6 +24,7 @@ import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; +import java.io.InterruptedIOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -106,6 +107,26 @@ private static String extractRequestId(Map> headers) { return (ids == null || ids.isEmpty()) ? null : ids.get(0); } + private static final int HTTP_REQUEST_TIMEOUT = 408; + private static final String REQUEST_TIMED_OUT_MESSAGE = "Request timed out."; + + /** + * Builds the {@link ErrorRecord} for a network-layer failure (a batch future that + * completed exceptionally with a non-API exception). A client-side timeout — OkHttp's + * call timeout surfaces as {@link InterruptedIOException} ({@link java.net.SocketTimeoutException} + * is a subclass) anywhere in the cause chain — is reported as HTTP 408 with a clean + * message, so callers can tell a timeout apart from a server 500. Any other network + * error keeps the existing 500 behaviour. + */ + private static ErrorRecord networkErrorRecord(Throwable ex, int index) { + for (Throwable t = ex; t != null; t = t.getCause()) { + if (t instanceof InterruptedIOException) { + return new ErrorRecord(index, REQUEST_TIMED_OUT_MESSAGE, HTTP_REQUEST_TIMEOUT); + } + } + return new ErrorRecord(index, ex.getMessage(), 500); + } + public static List handleBatchException( Throwable ex, List batch, int batchNumber, int batchSize ) { @@ -153,8 +174,7 @@ public static List handleBatchException( } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -210,8 +230,7 @@ public static List handleDetokenizeBatchException( } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -398,8 +417,7 @@ public static List handleDeleteTokensBatchException( } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -508,8 +526,7 @@ public static List handleTokenizeBatchException( int indexNumber = batchNumber * batchSize; int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; for (int j = 0; j < batchDataSize; j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } diff --git a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java index 145bc8d1..1d738266 100644 --- a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java @@ -33,30 +33,30 @@ public void clientWideConfigSetBeforeAddVaultReachesController() throws Exceptio Skyflow client = Skyflow.builder() .timeout(30) .maxRetries(2) - .initialRetryDelay(300L) - .maxRetryDelay(1500L) + .initialRetryDelayMillis(300L) + .maxRetryDelayMillis(1500L) .addVaultConfig(vaultConfig()) .build(); VaultController controller = client.vault(); Assert.assertEquals(Integer.valueOf(30), commonField(controller, "commonTimeout")); Assert.assertEquals(Integer.valueOf(2), commonField(controller, "commonMaxRetries")); - Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelay")); - Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelay")); + Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelayMillis")); } @Test public void clientWideConfigSetAfterAddVaultPropagatesToExistingController() throws Exception { Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(vaultConfig()); // Set config AFTER the controller already exists -> exercises propagateHttpConfig's loop. - builder.timeout(45).maxRetries(4).initialRetryDelay(700L).maxRetryDelay(3000L); + builder.timeout(45).maxRetries(4).initialRetryDelayMillis(700L).maxRetryDelayMillis(3000L); Skyflow client = builder.build(); VaultController controller = client.vault(); Assert.assertEquals(Integer.valueOf(45), commonField(controller, "commonTimeout")); Assert.assertEquals(Integer.valueOf(4), commonField(controller, "commonMaxRetries")); - Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelay")); - Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelay")); + Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelayMillis")); } @Test @@ -66,7 +66,7 @@ public void noClientWideConfigLeavesCommonFieldsNull() throws Exception { VaultController controller = client.vault(); Assert.assertNull(commonField(controller, "commonTimeout")); Assert.assertNull(commonField(controller, "commonMaxRetries")); - Assert.assertNull(commonField(controller, "commonInitialRetryDelay")); - Assert.assertNull(commonField(controller, "commonMaxRetryDelay")); + Assert.assertNull(commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertNull(commonField(controller, "commonMaxRetryDelayMillis")); } } diff --git a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java index b8c1933e..f206a437 100644 --- a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java @@ -93,18 +93,18 @@ public void vaultConfigStoresHttpFieldsAndDefaultsToNull() throws SkyflowExcepti VaultConfig cfg = new VaultConfig(); Assert.assertNull(cfg.getTimeout()); Assert.assertNull(cfg.getMaxRetries()); - Assert.assertNull(cfg.getInitialRetryDelay()); - Assert.assertNull(cfg.getMaxRetryDelay()); + Assert.assertNull(cfg.getInitialRetryDelayMillis()); + Assert.assertNull(cfg.getMaxRetryDelayMillis()); cfg.setTimeout(30); cfg.setMaxRetries(2); - cfg.setInitialRetryDelay(250L); - cfg.setMaxRetryDelay(1500L); + cfg.setInitialRetryDelayMillis(250L); + cfg.setMaxRetryDelayMillis(1500L); Assert.assertEquals(Integer.valueOf(30), cfg.getTimeout()); Assert.assertEquals(Integer.valueOf(2), cfg.getMaxRetries()); - Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelay()); - Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelay()); + Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelayMillis()); + Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelayMillis()); } private RetryInterceptor retryInterceptor(VaultClient client) throws Exception { @@ -145,8 +145,8 @@ public void defaultRetryConfigAppliedToInterceptor() throws Exception { public void vaultLevelRetryConfigOverridesDefault() throws Exception { VaultConfig cfg = apiKeyConfig(); cfg.setMaxRetries(1); - cfg.setInitialRetryDelay(250L); - cfg.setMaxRetryDelay(1200L); + cfg.setInitialRetryDelayMillis(250L); + cfg.setMaxRetryDelayMillis(1200L); VaultClient client = new VaultClient(cfg, cfg.getCredentials()); client.setBearerToken(); diff --git a/v3/src/test/java/com/skyflow/utils/UtilsTests.java b/v3/src/test/java/com/skyflow/utils/UtilsTests.java index be1456b1..da4eed19 100644 --- a/v3/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/v3/src/test/java/com/skyflow/utils/UtilsTests.java @@ -2211,4 +2211,72 @@ public void testFormatTokenizeResponseAbsentBatchData_returnsEmptySuccessAndErro Assert.assertTrue(result.getSuccess().isEmpty()); Assert.assertTrue(result.getErrors().isEmpty()); } + + // ── SK-3002: client-side timeout is classified as 408, not a generic 500 ── + + private static Throwable timeoutFailure() { + // Mirrors the real chain: OkHttp call-timeout (SocketTimeoutException, a + // subclass of InterruptedIOException) -> wrapped by the generated client in + // ApiClientException -> wrapped by CompletableFuture in CompletionException. + return new java.util.concurrent.CompletionException( + new com.skyflow.generated.rest.core.ApiClientException( + "Network error executing HTTP request", + new java.net.SocketTimeoutException("timeout"))); + } + + @Test + public void handleBatchException_timeout_yields408AndCleanMessage() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), + V1InsertRecordData.builder().build()); + + List errors = Utils.handleBatchException(timeoutFailure(), batch, 0, 1); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleDetokenizeBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(Arrays.asList("t1", "t2")).vaultId("v1").build(); + + List errors = Utils.handleDetokenizeBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleDeleteTokensBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(Arrays.asList("tok1", "tok2")).vaultId("v1").build(); + + List errors = Utils.handleDeleteTokensBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleTokenizeBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v2").build())) + .build(); + + List errors = Utils.handleTokenizeBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } } \ No newline at end of file From 7cdd8ce3b36cd219c88d83a0791bbc52a175f3b3 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 22 Jul 2026 12:29:29 +0000 Subject: [PATCH 6/6] [AUTOMATED] Private Release 2.1.1-dev-3196451 --- v3/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/pom.xml b/v3/pom.xml index c529d8bf..b0a3be70 100644 --- a/v3/pom.xml +++ b/v3/pom.xml @@ -11,7 +11,7 @@ skyflow-java - 2.1.1-dev.1236334 + 2.1.1-dev.3196451 jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language