From 43a7e07488d7de38b5b2929d2c1baa704db1aa8d Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Wed, 19 Aug 2026 11:46:39 +0530 Subject: [PATCH 1/6] chore: trigger internal-release workflow rerun Empty commits don't trigger this workflow (paths-ignore treats a zero-file diff as vacuously all-ignored), so this touches pom.xml directly instead. --- flowvault/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/flowvault/pom.xml b/flowvault/pom.xml index 5662beb9..8f8e089f 100644 --- a/flowvault/pom.xml +++ b/flowvault/pom.xml @@ -1,4 +1,5 @@ + From f430daecccd033361bb7b020c3d15ebd1c4d6f71 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 24 Aug 2026 13:18:39 +0530 Subject: [PATCH 2/6] SK-3061: fix stale nested Tokenize response type to match real API contract V1FlowTokenizeResponseObject modeled a value + nested tokens[] array that the real API has never sent since a 2026-03-17 contract flattening (proto commit 7a656c0f, aligning Tokenize's shape with Detokenize's). The generated type was built from a stale spec snapshot and never corrected, so the SDK only produced correct output via a hand-written flatToken() workaround that scavenged token/tokenGroupName/error/httpCode out of Jackson's additionalProperties catch-all instead of reading real fields. Confirmed against the live proto (skyflowapi/common), a captured production response fixture already in this repo's tests, and the actual skyflow-fern-config/schemaless OpenAPI spec - all agree the wire shape is flat: token/value/tokenGroupName/error/httpCode as siblings, one row per (value, token group). Changes: - V1FlowTokenizeResponseObject: replaced value+tokens(nested) with the real flat fields (token, value, tokenGroupName, error, httpCode), matching the sibling V1FlowDetokenizeResponseObject's existing shape/conventions. - Deleted FlowTokenizeResponseObjectToken (the nested per-token type is no longer referenced anywhere). - Utils.java: buildTokenizeResponseTokens() now reads the flat fields directly; deleted flatToken() and the nested-vs-flat branching entirely. groupTokenizeRows()/acceptsRow()/valuesMatch() are untouched - the value-matching/folding logic that reconstructs per-record grouping from flat rows was already correct and needed no changes. - Updated the 6 tests that built the old nested shape directly to build the flat shape instead; replaced one test whose entire premise ("if the API ever returns the nested shape") is no longer a real code path with equivalent flat-shape coverage of the same folding behavior. Deliberately scoped to only the Tokenize response type + its consumer in Utils.java: Insert/Detokenize/DeleteToken and the FlowserviceClient/ VaultClient accessor structure are completely untouched, since they have no known contract issue and a full client regeneration (evaluated and set aside) would have meant unnecessary risk for operations that already work. Verified: mvn test (708/708, excluding one pre-existing unrelated failure - common's TokenTests.testExpiredTokenForIsExpiredToken depends on an uncommitted .env secret); mvn verify/japicmp shows zero public API changes (only diff present is an unrelated, already-merged Token.getPath() addition from #413). Co-Authored-By: Claude Sonnet 5 --- .../FlowTokenizeResponseObjectToken.java | 188 ------------------ .../types/V1FlowTokenizeResponseObject.java | 138 +++++++++++-- .../main/java/com/skyflow/utils/Utils.java | 58 +----- .../utils/FlatTokenizeResponseTests.java | 11 +- .../java/com/skyflow/utils/UtilsTests.java | 24 +-- .../controller/VaultControllerTests.java | 16 +- 6 files changed, 140 insertions(+), 295 deletions(-) delete mode 100644 flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java deleted file mode 100644 index 0edfd9a8..00000000 --- a/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java +++ /dev/null @@ -1,188 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ -package com.skyflow.generated.rest.types; - -import com.fasterxml.jackson.annotation.*; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.skyflow.generated.rest.core.ObjectMappers; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -@JsonInclude(JsonInclude.Include.NON_ABSENT) -@JsonDeserialize(builder = FlowTokenizeResponseObjectToken.Builder.class) -public final class FlowTokenizeResponseObjectToken { - private final Optional tokenGroupName; - - private final Optional token; - - private final Optional error; - - private final Optional httpCode; - - private final Map additionalProperties; - - private FlowTokenizeResponseObjectToken( - Optional tokenGroupName, - Optional token, - Optional error, - Optional httpCode, - Map additionalProperties) { - this.tokenGroupName = tokenGroupName; - this.token = token; - this.error = error; - this.httpCode = httpCode; - this.additionalProperties = additionalProperties; - } - - /** - * @return Token group Name - */ - @JsonProperty("tokenGroupName") - public Optional getTokenGroupName() { - return tokenGroupName; - } - - /** - * @return Token value - */ - @JsonProperty("token") - public Optional getToken() { - return token; - } - - /** - * @return Error if tokenization failed - */ - @JsonProperty("error") - public Optional getError() { - return error; - } - - /** - * @return HTTP status code of the response - */ - @JsonProperty("httpCode") - public Optional getHttpCode() { - return httpCode; - } - - @Override - public boolean equals(Object other) { - if (this == other) return true; - return other instanceof FlowTokenizeResponseObjectToken && equalTo((FlowTokenizeResponseObjectToken) other); - } - - @JsonAnyGetter - public Map getAdditionalProperties() { - return this.additionalProperties; - } - - private boolean equalTo(FlowTokenizeResponseObjectToken other) { - return tokenGroupName.equals(other.tokenGroupName) - && token.equals(other.token) - && error.equals(other.error) - && httpCode.equals(other.httpCode); - } - - @Override - public int hashCode() { - return Objects.hash(this.tokenGroupName, this.token, this.error, this.httpCode); - } - - @Override - public String toString() { - return ObjectMappers.stringify(this); - } - - public static Builder builder() { - return new Builder(); - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class Builder { - private Optional tokenGroupName = Optional.empty(); - - private Optional token = Optional.empty(); - - private Optional error = Optional.empty(); - - private Optional httpCode = Optional.empty(); - - @JsonAnySetter - private Map additionalProperties = new HashMap<>(); - - private Builder() {} - - public Builder from(FlowTokenizeResponseObjectToken other) { - tokenGroupName(other.getTokenGroupName()); - token(other.getToken()); - error(other.getError()); - httpCode(other.getHttpCode()); - return this; - } - - /** - *

Token group Name

- */ - @JsonSetter(value = "tokenGroupName", nulls = Nulls.SKIP) - public Builder tokenGroupName(Optional tokenGroupName) { - this.tokenGroupName = tokenGroupName; - return this; - } - - public Builder tokenGroupName(String tokenGroupName) { - this.tokenGroupName = Optional.ofNullable(tokenGroupName); - return this; - } - - /** - *

Token value

- */ - @JsonSetter(value = "token", nulls = Nulls.SKIP) - public Builder token(Optional token) { - this.token = token; - return this; - } - - public Builder token(String token) { - this.token = Optional.ofNullable(token); - return this; - } - - /** - *

Error if tokenization failed

- */ - @JsonSetter(value = "error", nulls = Nulls.SKIP) - public Builder error(Optional error) { - this.error = error; - return this; - } - - public Builder error(String error) { - this.error = Optional.ofNullable(error); - return this; - } - - /** - *

HTTP status code of the response

- */ - @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) - public Builder httpCode(Optional httpCode) { - this.httpCode = httpCode; - return this; - } - - public Builder httpCode(Integer httpCode) { - this.httpCode = Optional.ofNullable(httpCode); - return this; - } - - public FlowTokenizeResponseObjectToken build() { - return new FlowTokenizeResponseObjectToken(tokenGroupName, token, error, httpCode, additionalProperties); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java index eb4a15ac..badc525e 100644 --- a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java @@ -7,28 +7,51 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1FlowTokenizeResponseObject.Builder.class) public final class V1FlowTokenizeResponseObject { + private final Optional token; + private final Optional value; - private final Optional> tokens; + private final Optional tokenGroupName; + + private final Optional error; + + private final Optional httpCode; private final Map additionalProperties; private V1FlowTokenizeResponseObject( + Optional token, Optional value, - Optional> tokens, + Optional tokenGroupName, + Optional error, + Optional httpCode, Map additionalProperties) { + this.token = token; this.value = value; - this.tokens = tokens; + this.tokenGroupName = tokenGroupName; + this.error = error; + this.httpCode = httpCode; this.additionalProperties = additionalProperties; } /** - * @return Value of token + * @return Token that was generated + */ + @JsonProperty("token") + public Optional getToken() { + return token; + } + + /** + * @return Value that was tokenized */ @JsonProperty("value") public Optional getValue() { @@ -36,11 +59,27 @@ public Optional getValue() { } /** - * @return Token value + * @return Name of the token group + */ + @JsonProperty("tokenGroupName") + public Optional getTokenGroupName() { + return tokenGroupName; + } + + /** + * @return Error if tokenization failed */ - @JsonProperty("tokens") - public Optional> getTokens() { - return tokens; + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; } @Override @@ -55,12 +94,16 @@ public Map getAdditionalProperties() { } private boolean equalTo(V1FlowTokenizeResponseObject other) { - return value.equals(other.value) && tokens.equals(other.tokens); + return token.equals(other.token) + && value.equals(other.value) + && tokenGroupName.equals(other.tokenGroupName) + && error.equals(other.error) + && httpCode.equals(other.httpCode); } @Override public int hashCode() { - return Objects.hash(this.value, this.tokens); + return Objects.hash(this.token, this.value, this.tokenGroupName, this.error, this.httpCode); } @Override @@ -74,9 +117,15 @@ public static Builder builder() { @JsonIgnoreProperties(ignoreUnknown = true) public static final class Builder { + private Optional token = Optional.empty(); + private Optional value = Optional.empty(); - private Optional> tokens = Optional.empty(); + private Optional tokenGroupName = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); @JsonAnySetter private Map additionalProperties = new HashMap<>(); @@ -84,13 +133,30 @@ public static final class Builder { private Builder() {} public Builder from(V1FlowTokenizeResponseObject other) { + token(other.getToken()); value(other.getValue()); - tokens(other.getTokens()); + tokenGroupName(other.getTokenGroupName()); + error(other.getError()); + httpCode(other.getHttpCode()); + return this; + } + + /** + *

Token that was generated

+ */ + @JsonSetter(value = "token", nulls = Nulls.SKIP) + public Builder token(Optional token) { + this.token = token; + return this; + } + + public Builder token(String token) { + this.token = Optional.ofNullable(token); return this; } /** - *

Value of token

+ *

Value that was tokenized

*/ @JsonSetter(value = "value", nulls = Nulls.SKIP) public Builder value(Optional value) { @@ -104,21 +170,49 @@ public Builder value(Object value) { } /** - *

Token value

+ *

Name of the token group

+ */ + @JsonSetter(value = "tokenGroupName", nulls = Nulls.SKIP) + public Builder tokenGroupName(Optional tokenGroupName) { + this.tokenGroupName = tokenGroupName; + return this; + } + + public Builder tokenGroupName(String tokenGroupName) { + this.tokenGroupName = Optional.ofNullable(tokenGroupName); + return this; + } + + /** + *

Error if tokenization failed

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

*/ - @JsonSetter(value = "tokens", nulls = Nulls.SKIP) - public Builder tokens(Optional> tokens) { - this.tokens = tokens; + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; return this; } - public Builder tokens(List tokens) { - this.tokens = Optional.ofNullable(tokens); + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); return this; } public V1FlowTokenizeResponseObject build() { - return new V1FlowTokenizeResponseObject(value, tokens, additionalProperties); + return new V1FlowTokenizeResponseObject(token, value, tokenGroupName, error, httpCode, additionalProperties); } } -} +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index 9d3f8e0f..d668f246 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -15,7 +15,6 @@ import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; import com.skyflow.generated.rest.types.FlowEnumUpdateType; -import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; @@ -852,57 +851,20 @@ private static V1FlowTokenizeRequestObject buildTokenizeRequestObject(TokenizeRe private static List buildTokenizeResponseTokens( V1FlowTokenizeResponseObject record, String requestId) { + boolean failed = record.getError().isPresent() + && record.getError().get() != null + && !record.getError().get().isEmpty(); List tokens = new ArrayList<>(); - if (record.getTokens().isPresent()) { - for (FlowTokenizeResponseObjectToken tokenObj : record.getTokens().get()) { - boolean failed = tokenObj.getError().isPresent() - && tokenObj.getError().get() != null - && !tokenObj.getError().get().isEmpty(); - tokens.add(new TokenizeResponseToken( - tokenObj.getTokenGroupName().orElse(null), - tokenObj.getToken().orElse(null), - tokenObj.getHttpCode().orElse(failed ? 500 : 200), - failed ? tokenObj.getError().get() : null, - requestId - )); - } - } else { - // the API reports one flat row per (value, token group) instead of a nested tokens - // array; the generated type has no fields for those, so they land in additionalProperties - TokenizeResponseToken flat = flatToken(record, requestId); - if (flat != null) { - tokens.add(flat); - } - } + tokens.add(new TokenizeResponseToken( + asNonEmptyString(record.getTokenGroupName().orElse(null)), + asNonEmptyString(record.getToken().orElse(null)), + record.getHttpCode().orElse(failed ? 500 : 200), + failed ? record.getError().get() : null, + requestId + )); return tokens; } - /** - * Reads a flat {@code tokenGroupName}/{@code token}/{@code error}/{@code httpCode} row out of - * the wire object's unmodelled properties. Returns null when the row carries none of them, so a - * genuinely token-less record still reports an empty list rather than a phantom entry. - */ - private static TokenizeResponseToken flatToken(V1FlowTokenizeResponseObject record, String requestId) { - Map extras = record.getAdditionalProperties(); - if (extras == null || extras.isEmpty()) { - return null; - } - boolean carriesTokenFields = extras.containsKey("token") - || extras.containsKey("tokenGroupName") - || extras.containsKey("error") - || extras.containsKey("httpCode"); - if (!carriesTokenFields) { - return null; - } - String error = asNonEmptyString(extras.get("error")); - String token = asNonEmptyString(extras.get("token")); - Integer httpCode = extras.get("httpCode") instanceof Number - ? ((Number) extras.get("httpCode")).intValue() - : (error != null ? 500 : 200); - return new TokenizeResponseToken( - asNonEmptyString(extras.get("tokenGroupName")), token, httpCode, error, requestId); - } - /** The API sends "" for a token or error that does not apply; normalise both to null. */ private static String asNonEmptyString(Object value) { if (!(value instanceof String)) { diff --git a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java index 43d51408..5c2994ae 100644 --- a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java @@ -202,14 +202,11 @@ public void testMoreRowsThanTheRequestExplains_rowsAreKeptNotDropped() { } @Test - public void testGroupedResponseShape_stillFoldsOneRowPerRecord() { - // if the API ever returns the nested shape the wire type models, nothing changes + public void testMultipleGroupsForOneValue_foldOntoOneRecord() { String json = "{\"response\": [" - + "{\"value\": \"v0\", \"tokens\": [" - + " {\"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," - + " {\"tokenGroupName\": \"g2\", \"token\": \"tok-b\", \"httpCode\": 200}]}," - + "{\"value\": \"v1\", \"tokens\": [" - + " {\"tokenGroupName\": \"g1\", \"token\": \"tok-c\", \"httpCode\": 200}]}" + + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," + + "{\"value\": \"v0\", \"tokenGroupName\": \"g2\", \"token\": \"tok-b\", \"httpCode\": 200}," + + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-c\", \"httpCode\": 200}" + "]}"; List sent = Arrays.asList( record("v0", "g1", "g2"), record("v1", "g1")); diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 80fc8361..9a2bd46a 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -11,7 +11,6 @@ import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; import com.skyflow.generated.rest.types.FlowEnumUpdateType; -import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; @@ -1616,13 +1615,11 @@ public void testHandleBulkTokenizeBatchException_noTokenGroupsStillReportsOneEnt public void testHandleBulkTokenizeBatchException_errorBodyWithResponseArrayRebuildsRecords() { // A 4xx whose body echoes the per-row "response" array is rebuilt via tokenizeRecordsFromErrorBody // rather than summarized by the bare status code. - Map tokenRow = new HashMap<>(); - tokenRow.put("tokenGroupName", "group1"); - tokenRow.put("error", "BYOT token should contain one token group"); - tokenRow.put("httpCode", 400); Map responseRow = new HashMap<>(); responseRow.put("value", "v1"); - responseRow.put("tokens", Collections.singletonList(tokenRow)); + responseRow.put("tokenGroupName", "group1"); + responseRow.put("error", "BYOT token should contain one token group"); + responseRow.put("httpCode", 400); Map body = new HashMap<>(); body.put("response", Collections.singletonList(responseRow)); ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body); @@ -1941,10 +1938,7 @@ private static V1FlowTokenizeResponse tokenizeWire(V1FlowTokenizeResponseObject. @Test public void testFormatBulkTokenizeResponse_success() { V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").token("tok-abc").build())) - .build()); + .value("value1").tokenGroupName("group1").token("tok-abc").build()); BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); @@ -1960,10 +1954,7 @@ public void testFormatBulkTokenizeResponse_success() { @Test public void testFormatBulkTokenizeResponse_tokenError() { V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").error("invalid value").httpCode(400).build())) - .build()); + .value("value1").tokenGroupName("group1").error("invalid value").httpCode(400).build()); BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); @@ -1977,10 +1968,7 @@ public void testFormatBulkTokenizeResponse_tokenError() { @Test public void testFormatBulkTokenizeResponse_derivesIndexFromBatchPosition() { V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() - .value("value1") - .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").token("tok-abc").build())) - .build()); + .value("value1").tokenGroupName("group1").token("tok-abc").build()); // this batch starts at index 40 in the caller's list BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 6b4aa516..2f1cd558 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -12,7 +12,6 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; import com.skyflow.generated.rest.resources.flowservice.RawFlowserviceClient; -import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; @@ -392,10 +391,8 @@ public void testBulkTokenize_success() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").token("tok-abc").build(); V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1").tokens(Collections.singletonList(token)).build(); + .value("value1").tokenGroupName("group1").token("tok-abc").build(); V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() .response(Collections.singletonList(responseObject)).build(); ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); @@ -433,10 +430,8 @@ public void testBulkTokenizeAsync_success() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class); RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1").token("tok-abc").build(); V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() - .value("value1").tokens(Collections.singletonList(token)).build(); + .value("value1").tokenGroupName("group1").token("tok-abc").build(); V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() .response(Collections.singletonList(responseObject)).build(); ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); @@ -896,13 +891,10 @@ private static void stubTokenizeEcho(RawFlowserviceClient mockRaw) { invocation.getArgument(0); List responseRecords = new ArrayList<>(); for (com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj : req.getData().get()) { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-" + obj.getValue().get()) - .build(); responseRecords.add(V1FlowTokenizeResponseObject.builder() .value(obj.getValue().get()) - .tokens(Collections.singletonList(token)) + .tokenGroupName("group1") + .token("tok-" + obj.getValue().get()) .build()); } V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder().response(responseRecords).build(); From cb7261b6bcbded0616d0af567d48315bc73d1755 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 24 Aug 2026 14:54:10 +0530 Subject: [PATCH 3/6] SK-3061: flatten public BulkTokenizeResponse to match the API's row shape BulkTokenizeResponse.getRecords() previously grouped rows back into a records[].tokens[] structure keyed by submitted value. The real API has never nested this way - it returns one flat row per (value, token group) pair. This drops the SDK-side grouping: BulkTokenizeResponseRecord and TokenizeResponseRecord are now flat (value, tokenGroupName, token, httpCode, error, requestId) with index kept on BulkTokenizeResponseRecord so rows from the same submitted value can still be correlated. Summary classification (totalTokenized/totalPartial/totalFailed) and getRecordsToRetry() are recomputed by grouping on index internally, including the case where a value gets zero rows back. TokenizeResponseToken is removed; its fields fold directly into TokenizeResponseRecord. Utils.java's row-to-index correlation (acceptsRow/valuesMatch/batch-splitting on duplicate values) is unchanged - only the final emission step is flat instead of nested. Samples and README updated to the flat shape. This is an intentional breaking change to BulkTokenizeResponse's public constructors; the japicmp baseline is regenerated accordingly. --- flowvault/README.md | 46 +++---- .../skyflow-flowvault-java.baseline.jar | Bin 470542 -> 466751 bytes .../com/example/vault/BulkTokenizeAsync.java | 17 ++- .../com/example/vault/BulkTokenizeSync.java | 17 ++- .../main/java/com/skyflow/utils/Utils.java | 50 +++----- .../vault/controller/VaultController.java | 1 - .../vault/data/BulkTokenizeResponse.java | 82 +++++++------ .../data/BulkTokenizeResponseRecord.java | 11 +- .../vault/data/TokenizeResponseRecord.java | 60 ++++++++-- .../vault/data/TokenizeResponseToken.java | 72 ----------- .../utils/FlatTokenizeResponseTests.java | 113 +++++++++--------- .../com/skyflow/utils/RequestIdTests.java | 46 ++++--- .../java/com/skyflow/utils/UtilsTests.java | 59 +++++---- .../controller/VaultControllerTests.java | 16 ++- .../skyflow/vault/data/BulkResponseTests.java | 71 +++++------ .../vault/data/BulkRetryAndSummaryTests.java | 79 +++++------- .../data/RequestResponseWrapperTests.java | 10 +- .../vault/data/ResponseComponentTests.java | 46 +++---- 18 files changed, 341 insertions(+), 455 deletions(-) delete mode 100644 flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java diff --git a/flowvault/README.md b/flowvault/README.md index f04ece9e..13778be3 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -562,34 +562,20 @@ Sample response: { "summary": { "totalTokens": 2, "totalTokenized": 1, "totalPartial": 0, "totalFailed": 1 }, "records": [ - { - "index": 0, - "value": "4111111111111111", - "tokens": [ - { "tokenGroupName": "card_number_cg", "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null } - ] - }, - { - "index": 1, - "value": "john.doe@example.com", - "tokens": [ - { "tokenGroupName": "email_cg", "token": null, "httpCode": 400, "error": "Token group email_cg not found.", "requestId": "a1b2c3d4-..." } - ] - } + { "index": 0, "value": "4111111111111111", "tokenGroupName": "card_number_cg", "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null }, + { "index": 1, "value": "john.doe@example.com", "tokenGroupName": "email_cg", "token": null, "httpCode": 400, "error": "Token group email_cg not found.", "requestId": "a1b2c3d4-..." } ] } ``` -Tokenize reports at **two** levels: one entry per input value in `records`, and inside each of those, one entry per requested token group in `tokens`. Because a single value can map to several token groups, the summary distinguishes fully tokenized values (`totalTokenized`), partially tokenized values where some groups succeeded and others failed (`totalPartial`), and fully failed values (`totalFailed`). The three always add up to `totalTokens`, which counts input values, not tokens produced. +`records` is flat: one entry per (value, token group) outcome, matching the API's own response shape. Because a single value can map to several token groups, several entries can share the same `index` — that's how the SDK tells you which input value an entry belongs to. The summary classifies by index rather than by entry: fully tokenized values (`totalTokenized`), partially tokenized values where some groups succeeded and others failed (`totalPartial`), and fully failed values (`totalFailed`). The three always add up to `totalTokens`, which counts input values, not entries. ```java for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() == null) { - System.out.println(record.getValue() + " -> " + token.getTokenGroupName() + " = " + token.getToken()); - } else { - System.out.println(record.getValue() + " -> " + token.getTokenGroupName() + " failed: " + token.getError()); - } + if (record.getError() == null) { + System.out.println(record.getValue() + " -> " + record.getTokenGroupName() + " = " + record.getToken()); + } else { + System.out.println(record.getValue() + " -> " + record.getTokenGroupName() + " failed: " + record.getError()); } } ``` @@ -805,7 +791,7 @@ Every bulk response exposes `getSummary()` and `getRecords()`. The records list | `getError()` | failures only | Error message for this item. `null` means this item succeeded. | | `getRequestId()` | failures only | The `x-request-id` of the batch this item was in — quote it in support escalations. Items from the same batch share one id. | -The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getTokens()`/`getData()` for insert (`getFields()` is deprecated — it returns the same data in its original, pre-typed `Map` shape, not `getTokens()`'s `Token` objects), `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. +The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getTokens()`/`getData()` for insert (`getFields()` is deprecated — it returns the same data in its original, pre-typed `Map` shape, not `getTokens()`'s `Token` objects), `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getValue()`/`getTokenGroupName()`/`getToken()` for tokenize, `getToken()` for delete. Summaries per operation: @@ -835,18 +821,16 @@ for (BulkInsertResponseRecord record : response.getRecords()) { } ``` -For tokenize, the check is one level deeper, because a single value can partially succeed: +Tokenize reports one entry per (value, token group) outcome, so a single value can partially succeed — several entries share its `index`: ```java for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() == null) { - System.out.println(record.getIndex() + "/" + token.getTokenGroupName() - + " -> " + token.getToken()); - } else { - System.err.println(record.getIndex() + "/" + token.getTokenGroupName() - + " failed [" + token.getHttpCode() + "] " + token.getError()); - } + if (record.getError() == null) { + System.out.println(record.getIndex() + "/" + record.getTokenGroupName() + + " -> " + record.getToken()); + } else { + System.err.println(record.getIndex() + "/" + record.getTokenGroupName() + + " failed [" + record.getHttpCode() + "] " + record.getError()); } } ``` diff --git a/flowvault/api-report/skyflow-flowvault-java.baseline.jar b/flowvault/api-report/skyflow-flowvault-java.baseline.jar index ecdba5401713dac1a355b9f191873cb8b6fc4cfc..1294715f6ed503635d12e0d5584808b7a14690c5 100644 GIT binary patch delta 58157 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 z zJr~n*vrhlEf>w>Val`WgOZJPOV5F+aLEDu`_V>;c8o&byN?a2$@s(7EY8eRpLcWcv zV*=xi?OdiDx8tY?guc*THbpxHk}R7do^BSsstMSA!uv@QE2JrOfBM4i`cNi$X+03X z^o|^i&`rx$fZUQv#Ril1eUrl**Doo@!i9{T1*X2|fGfG-dEwSZNmmTIfc!jB8f4SuiG4=s;g1t3|G!Czmm#OT$+$y`fD zzTdjGR!j@i?!y>$gyn`kZhS)*5_7MlBQPm2GQVkQVR(qE_lfqwuR1Ns3ePYo;tk#O zjow@3U$WoXfI17u5v5#F-sGWF@|W&$kcM9!*m`#`olqmOM-&3MYp$h%#5Iu`S6 zjq0-BWd(b`L~JcV(Y~bkK3cQ}02N!BRKBqmuB7F%5E?vTR#GgETItq;F-lEo#cs_F6W;Ey=JSL{q8M#N| zo}zv7w)!YYQL#!x6JPn^YV}j46|OI5HAnb?59_7B*rh-JWyj8vQHGsLPE%LHG>l0` zU|b+6<*``E6=D14$rAd*ue_wUeHZiX5|_iCNaU2CPkzGxf#vvQkC8v%-@NI>`aiH7 z{0Bw+c?p68xvv!a9{~LuitqlzSGIr9jxsSMux{@kPBA0-jkwITD8cll04m5(!_Ysp z@&7?C3OKtIfDHO!h6dSc{s(mbqWk}XA+TfSZ+KVT`i}vC|JZ|YbVU8L{ulCYbshZ! zn}1N@e_%QaSfKp38d{I$KYs+IY=58?DkQs4;cpM*ctHPuA+$g5fW&KlL+0R-yMHL^ zH^TteRsaY=?PJ4#&HweKZh{0hO93E*$tnRTz(#Fj_ph=zq9^Ptaaho)pE4vV#B29p zC6FC$DI7of2O)AR3~}G)aYuC$V7_xJ`uKG9`RUu#{HU_<*WtIf^mlo;=VO?$o#uy9 z!}rHe8}9enem|;zzC15&Lap`QP-)5&kED)J4cE?~2zfUAcvWJ5SlsaP&8{_!b2-g2 ziw8cRqx(n8t@p$quAubcNu76{v|B{x8J2Z>-jlAnzcju2|KN3w8U*?@$s9hNdHM1A z_pXZgvl}#ONRz?A$T0fHn7auGOSNe_;*@r{Vq*&oCaimPkXZ|GmuvT_W(b5pP_QKz z+wcIL=fz(M`_Si4-2d)~xEb43SZ&POnujC~yd0!z_I4L!^;p-ztV$9bjGiz;Gnq5RZ_}UP$J>rJ{$45OB+}g#}}#}x=fhEawak3#?MO12UV(R^ArdzQR1Zoy%G2lYdWxJ>ksby-0ZGl*`o>bpdrB@?ugfSh_rInQUxIaHziAf%#(sH5{qI^dc zQQu^@fSE`z&%{w>P73wBtak3S0u@6v^`2D*GkXKElTR3B3S+FJAdl#O3LA|(NnkO|n>S+!|EI7) z5~pbYm6R*;-gK7xTfki#bjF; z`a6(d$N)A=1)zX!ssRiT!fz-4$_W7}1B89;<3D5nZZH1v>c3UzaD(OVNJ`hgx7Ofl zA}C~#%=W)m*RDv^e>#Z3JqYjsP{}qL5Ca-2+z=t*>!%TObsWi0HVAUi1nREx;T#E8 z7F^qxu$ayo5=$`7+WJc{5}-FoWXvrmjTqOi81DUT6 z+A{23!gx*8vdAf~q@A;t#GvvxAOq=Tb9&9ohK}D;j;=fpkcmZfwBweHzY4syuf`@x zC=AvLv6|_#bG-JmC%QR^=th{biwCw1Z*El;vcqsetY#P9ntqI>?{1sb0|S{})9P%> zO6J7gBDRU^;cD_80&Edl7bU^b!2QUk2V#F--fRG*bI1#}kxyNvyNStF5?x*|uLRJU z%imYGs8}xgF&oglsg<(1WsB3u>wRtQH)Wg8&VBr9VoHiz&1QGKs2z!f9J4VH_;HBl z_1hKGt;8X*Ezhx9!KU$nvxm4CY^>_OM$MXzQ}-$&j9D&H2#){&Kz$nt+>n9&ys0PK zR9Yc>C=qLPY;rzTvGbcj%tQ^WodIBez)Lw3vKFs>n`n~KTwohR{ zp1UO9_tJz42{Mvs4jwGxKKY;uI5rXHSD}|%yt$c=$jg!h!oarnK!2jg83a_>KG*N$ z=VOX$qQ+W@E3>i&(WYzKKt0B(eYNW-4af4Ln^6~$7+v!ifTcN!q@uhffIm08^t z4L4Y0@!{SbP(B`2G&AHoc1$Uh3LD0xyqJqnOwm%QxA;&_d=q^F#Cuj`2ud(8GSKB| z_cwn$-6_+G{(!GScfk~yo+BiRSZu4#Lc*AcAqk)5LX=#}VH~2F8pOdc$6ppWMPX;-5af&USx1_Xk4RMb{F07Sk zBIGM(JynItdG%EiFeMZ&^2Y?v%7DObBms74hX6pm$M!9Y`ZfycR44?4%d%73t8Rs? zPZyV$fpuU|u|wOFD-+cT7tdB5F?5zKuLLt$_M(*xHaks+rKd$d#Et@=pm;c|7!Yql zF40|Ime;GL*J@{Byg<|3?WT~{aalNFA*?Nfj!SrLK|LP{WCGjOqS#2c^Z34iM9u+f zQmxKuC8|xf)g$U9LFgkA?dp20u1Q3M`!2GTnctmm^>@pVq!VDip|w%vKk?cG?;&)S zWg{r5UF%dywv-h(*<$an(jY(W+S`@h^6W=Lv`^ifi$2GViO`Cqm2XVVgB0HBt;@w1ShIM&RU4 z`I)e?)U!uq1f{1rC4-for`exusMpWSRJpWe&=PkIT*j59IsK8vgx$|Y9bJpbp->CO zSMCx5;O}Ec;lz==lR-u80*Lb6a;-GE44sa?zxA=xFmFY`e5H~K^&#c;hf&7X*v9+4 zAe59c*8vnENX?WebtV^>)8BIVR`2>TdKVQs#sn>i-#mlQ{+gx3`6`vQ#HVc zNP~#Lj4hMA-3#oU`sKN^Yg*JU+;bV;>??EVA-4U}+OTChZ%L}!yt3?}&AD>ZJ{YDK z#Sr*y%BUPl9S!@^cA?cLm`y1;Jux8@oSO*1Dy$!!56qpwcHnY#FB+!F$Jfo}!|MBp zb5XW7eyK3L(CyxAZ9vOiUq9 z=`}!L-~gMs?r$EIn9%7f7b4cv{=N7oZFXCvZ>*MM3E}>G&L6U2V&N?rgI?%|=l4MX z_-!eq`@VHLzZXeUg*#7`j19aA!suZjocl1(dM%z)nwx)RZ%(Mu1!rri|B$Ur(cn#V z@C{PpwifD77Oq>~yAIkCnXka=Uehh)(4V@AWV6(viLUA5Hgh#wHB2umAvIz8&F>f@ z0|hBR$!jC6$#QZ5m$nS^njtEkq#ZO-`<{J*m*}WJ3AenCo(FX#djo5FySK%FI0mS2 z#2TH|7S1tV^Sg%fHelG2ne7pS%*&9OTCpKJ4Vd>GC=_DW2W0@YHFuv zzF#l%rB#NG;xhfPc?(anLeY$)p|m=vq^Z&#SYtS{E?o_vBP!WGYN8%1R9*=fTW;%V z1{Nk}X11(0m$`xKK3kdhOrD6q%RCk(04t}6s&cP3WsqGG2Wor8vQ)pvX(UH=fu+QG z2ZI2P!T^MaKSgX%#zd`B;GADsA^q@H zTwag5ufYxBR_K?++;nHX5>4f_3-vNf4A*jD`nZX*hq-ss`?CKq+@jUj`YxVux-NA` zFq^1-odvT~y8lRDk%kCsHssx1YT%(wf{k=PFiv89F_CXe8PysXSxe?Z)1F{bY%Vw@ zA&-6lPhD#Ih@Bu^c_WtjxuilrZ`dx(iVszYh*#QTG<9v!sY(!76kZ{HIl>bnN1@fw6Lf$$ zCMwpH&f?*h7mmMew}Xki4yZrU_H&*Xrxqh+{U&Y>V^Rs+Hjs#{<~tBP;pxC(mpc3~ z*dP&s??qK>X@cOsg-c`-sTV0+L#GEf#_eM#KTx{tKEX>=J#3HZyBvK!G=;y;mu}d5 z0$1TEuJ0*biMQ=~RyDMZO0>b?l;wh@H|oN-QMf(njeKNRV|v-qg1X54UDvM8HS_j~ zu0v&TmA3;3b3xM;IYH&b&_b-V_Y*!ljn~x3wK@F9h#~Js5B)*ENU5Eed(|}*WNEEt zn-*Lky?(v zv5^MSb~o~@M&&fmU2LYeW!q$HN(oK6nuZsA0EvoZQ~C9=BxHvr11aPwraETDHHqO8 zUVlIiuGe^gg_o@U(nRGucbWYI1mhO9Qw4^mR@#!C8w5yvSDVY0Gp43w_%PziLq0() zcmNfcA-`g3q>MVGcj?8{RlaTi;JbYPnNL*6Wn5vi5k#88E%gI-bXIMEKP+-X(tG2) z{xHwnzhvZx1LV2M$ak1mke0*`+^jH+g?Se_&v`%hiH+TzNSe2Y(pX@G7OI= zxlr7!0+Z+hhgzh(#UH#cZ_u&CSbu}eg#=|FOI&>#)6Zv;zUH@cmzFQsoXoSP%hfqO zjtrxIId%`$FEJ!kEutSRL~WND9WQ3Sc9ThtD7uB?n$3EJJ*4d2`r(|#!l|J8Q^ z#Egnlj2{`lJxVg&WWKdJsDvF@S-5w!JbzBz|FM+Owh;NNSBmLp?smMlfR&i|O1W%z ziS8!-L75?0lT5!+SENAlL;`1uYb+xR)HXp4dw2TQLIXp0{g~m2Q)MBH&RY$upH?|0 z^_sib!l>a`huvu?fcku`E&BCCJYOWWHq?13moD(XBz~YzHh7}R zS8b$tx0q5?UYRj8HxL--gee#rTyZn~af!z#DHr)9ug+r;BvLWE`vasiveA{kM`ien zk2XKVc64I!skD-vlj%ebyN|EFx!aKr<8kHFOkIl9o>|E6`bL!RB5S*yxdMq+1R^uz zeZf&#YoSL@yiLylHuXJJY=YEn;TBbFcIa1)N-b0cbJY0di_JF_fbmse7; zHiJE}V^y$x#Y9|H$LZi;R*-+X4D@Kuq3i8fR4P^-Ddf;*Aqos0LXxg{#siPN>nt)w zww}u7Z1#4WwMxm7Z$kZ|{Y|@lI7`y>@TU9lcl-NTi}+Xp=p* zZ|?vl43igMr%B33`c>DjkFH?M|sQVw6fM7+8%8>Y`<;CCpnLqC2E54}cJy6l0{B9@LF)~uacaBp|)8nRleC}x?Z~GM6-z)u66EFYE;c-QmP+W z*Tn(!4j*hvpr~jzs(^KV15Ir>Y82`1_IA36ChdBbKkA-sC(2n?*Rv0kMa9JyXo4Ky zcWbY}5M_=A%7FR{noxQD>)gZ5<;$-qJ0#wtf39=~OhwIft60WI(H@)h(jM<@WxrD^ z2sz6wi&llVr4c2=C;J+6sn|jGby0!4b-1o3jph~|AiN~)mS>a~#)3xmWFilDsu)6pumu;z8Va%^x z?wzmf#e86_=yae|BR?$J?Dg_m>3K}sGX$UKKEFjS&zCsb?&P&X+RtQhnYph|!Ogg8 z-uMJl&U9hf{uKD}dcN5A^{9Wr@wyPG(wS;ccUyocSmpz)sq+n)e?69uCnm08W5~A@!?MC^oP%&gMBLcFLZwnP)}lJ|&g> zSBh0&q15EZ`kHOGlmP7M5PjX%@>9dmM9voR7LuPt=Xi7vB||?4yXbe>zbK(5_7QGp zrW&!FuGH`B)WRq1_;9GqJOFt-{uKw?f%OGV4KT3yQeq-SPGYTvU}YY_S_I7sgE2HD z5QC`{JwGQwBo-dcZwXt6m5sW6otlVM*GR8kbXsV-~c?5-`Fy{K3)<#128P8mavS$YZ;+i_B6&U}GYE)sj8iB;R8AJEz)F2>!vlg zO|g0kQQ$PC{20(Tx#JE})E+1}M=)0E_(=A;2%h&xu%+!%^qMr{|CrrH43GN@i#=^? z`Qgh4`6broE|8_VUrlE&GE)dUB+4!5?xP-pDyVLNIF|{=8J~!Z`ksMrNRXcUAP)g} zv*Q!5L}EumIY``w04DX5LFjPfBy7*!ReI~D(pUP}P$2M}BD7hrz(OA=PR-fPka_Ks?fYGKChHpXc`mGcvuM(Ti zos*yXu()K{u`1R0CQ6m5O+|beCDmhACa46Jgv`>AsjqgKj6gh5QrB)?*)sWawVoj{9_)F=ci*Ofk3@el^$$kJrTHLqmaPA z@73eN$t~yS>~ZA1eP%ZB;TBf0jW5)jt4`k4KL+Dnbr?3D7c{*zN_}Z*le>KHqD}tq z6H7KU<;&1)+22LcWlghd&_NAUm$8Xh^Dd9(^qa9f7+T3l?fht5g0r@tAd{+vqNgG9 zme3@4ko=%&o6a?vx!oR*8R_0L{MSmVEs&mmOH_9w&R6Z=Z|$wYW}gr`_8J_Feji$=eE1R5S%CopoKK^gp1$Sv4XY`8 zVGJddYsHPQC5>f}li3~$>Q_ai&?vmb|7D*+VG&2tG>>!lA>|BJq@=xg$UiJ;HzdBJ?@%q6eK7;(uJkyaJ7nEnNQxB zE_U9O2rw+75#CB(Lky2xzo-vogxQ0gyXiH;%T47Wuaw2~Lf!WaztSZ$RhrC`H@*N2 z1ASO53;mrfNMa#|p%HcFnAxl@Fqag6W*7vA9A zfia@p=BljsNiN0SgLJE8=2Uz78t4+hr0E0aTmF`l62BKEf1OxkY+M(nQVyN%^n3qXWQ&I$I!`#k2g==BsHpuP-^hW+b({+$BdE>&l+?X^PbPz+X%^E=SG^Sw;F zx&y)aGEkv&tf^))O2qK6x{UGWNpx2W7uURH(r}L|-s<;PX0(EN(L3`GVvpULxGHav zm+M^B-oKW%ROh#7#{!3cC6*UPZhDJid1wWRN(UVg{SyAgr*jm>!Py1v_>zkBF;eo` z=EeS}O8K1H&)O%<8$w)RaU5awUDN9t%@j)v)i_|yGD4r4PPnv>hOO%o?E6wJ!eSJ? z6lIlBtP36zQtRlu(X5|eW3HVP%9E%!J=f~mFWwE7xxClY5!5B1G~G45oGlxZzbXxs z<8>|PVme)!KU9wsk>2Gr;CUZ2B(|GV!c=&`W|xC3tdlj9iz|?FWig|Q;-?_4+&r&| zOEm`M2Xo=nAZWV0(Ij>avs4&nI;C6-#A9QorXSqdW7oogI}Pxa8|?uC|U zNwhOA5fDr+uY*US{ZWyen($6pJv7H8pjp2XhgYVLfLxWe3Bx+${0zu zm_eO8{7SiTf;3VRW$8~80|b1ms;DG3rM6ER}VE&W@=nxJH9;6=&Z-1XCddKo1ktt_btz-zkT}q~p z(KnGLq*|qr`?p zI=t%fVH9hwnS^AM66T6Aq7SP}Z`0gaEnf{Gn|*i%Qu3Kg0QcHF68}ZG3a-a3@LuG} z9js&dBAMU816FnN}6aYe>SYdvp%-`}|XgBA6=Q<@dcJYumh4Hdf|P1I|L>X2b4( zdOrHqul;U7usTQU-7kz&k$!LzBlY%!wg3rx6lz?!8auX3Kg4(T0XHRY?rOgPMhS7$ z6rZLI>k)MMqWZuZQnIzD`;f)VX^r0@ao2OP`^Ety`Ab9Ml_++Y5~7a%_XAWbfh!+; zk~cC2Hf)7oHyb@XNb@c=>w$CcL$jG}BC2Uljbut&i4@I6^DYe9oJ3O^A}@W2QIF1c zeoU4P2ye?WU7)pO{M?WN^7q5I)WAh*snEo6CZ^g_)Zskc5Sb|Pnb=y?4-I^eO;Bd+ zb@!4ID)PsQoZLs?%aC{AOVC%djg33p%Z7=>%2-Lj2%lsjc5o6>2?N4l+SugYN^IDS z`(1p7In&elw11#t-ruZ@E&J#aby~qCL^WyBV9%oQ^78^$E}8zNW5 zwHP1u*HXrF&xlB~q}rF1=hi>K6O zb^G!cYd4rJK`*3iX_ECjRV`QaP-+Gkx{BL+W{+ zhZ>^+Q(UiG1 z2^Qzln3953xOxP#p+-Ku#FWOq)mVO|{C=%f(uS;X`sU>e#S`?NcVyqi_zba0(DtS9 zNRS>Sx=BHhKWWsq3Q~-7KeRVIUN3pX6BxIT-4(65^J;pD% z>=5=+I8)8)nl0=H=B!6z(${piKq=M-JgqI^$e=qKsL%btBCJKEg+&VUEb))J3i|Y# zRgje%=}4vp;jkPfM)-wHdRwl@Vmr&c>+>BNri?~(!mSH)zs};kzk(t{fgZ2!@p_F z%VY^0Tip6?kD|KKLK@F6KsXE#K|>zdbqW6cwKZoj$kpu=$Td9-N(h0WM2GM776C-LB1{bBx!|F}qsycerJ}~BN~KPOHF~qG z1wCp9VkSP-X-h#l3}4matA!q1(RicN_rL?ax%|URdhi7>yZRI^)mWtAqB(F&^hxCQW%Ezx`lAY@+$7f_cqD3_4qtJ02 z#|!qIeU*2Lkj0AAQvNL_vMk@Ciqw*PEoTZ-k7Hi<2ANJhP!7xX(z52ojtf3Z})t@7s|>3h&gH-E38Nl#TF+lFO(MChtvi` zn>nXgK^+5I$}M~40@DIw& z+z7lpUyih<-_CY`8)bVq-U^&m(WSdkSvthb?s=wJuXp6VY%-aAx?F*ly(r-ANzt(@ zvTVx4w77ABtO%X`%qvDC@q<{4?6&!SYVX>ZRMOi>3DsEMT1(l}NWt@fuBn`_~_|(r}S0YoS+4X2ww){7}uL&J9cY>L+EE^JFa9ED_Dyi zGK{HCvlJ_QQa68nQLH#I>?2Zf5B{g1yRsNvC0_132Ay8(OtfveP;&hWD)t@-YwZ@$ zvx`ZNTdy{W1qKe0C+bG4G}NV1e#ynseL&_uOS?e1e#6L?H+D8k%jkdx=)(V^x|d}e zZ>)Zy(v6F@Zo4G>2E&u(C3?uaUbTDV0Cf`4Gua zhBcXu0B64);}~b_sA_fTF7J<79we$2m!o!ls5otvTZP!iBD>0+?}a}7ews5oJce@; z-2RsO(>n~xP$LU=@qZX@aRI;exG|`M?ryouIuu??IMJy~D1BU!((2Tm3lLog_(y$w z5C$}pWoCn|xW=j>ZX_+An%84*k^BQ*r8Zd*%UJ^2=p%;S->Tl*k}vDTwZB=12E;EM z3s{tark{n3=CzEzL^h`Fzv25He$JDE@6v9g*u~{ zJkBcrHeXaqRu_@=cjnzFdGQP`R8zaQ5k7#!6*RRo(H|kS!o8?yoPha5;w25~rFqP; zh-U8%ElhXWxV83o&u%HBx5RgYe{-~VFfg@A&ZuqqXrSAC4cw{V9yPI832#g@w#Q>p zFp*mV)4wJ#uity#`1{#-VpVjkqROkGM7N($^%F9M<%d=Ed^RfkbzH%ptc0xfJbPD> zYUp{yK3a|~aw;^XRvdQtun4Zb+Z06O+)=xaOaJ*rcFFyg zxMWoJm~?&O7X9klCoZD0;~)R%xZfh1S{A{#jNoV29MT7iRY+i*`nPdiZKeElzx#|8 zs4zWl#pdGU;&3K!Z9C!vJ08#OO-Te) zbrmR>B$D1JixXDk-qYgJGgtX>uGFMC>P57L|8@Yhg`0=(fIa@t{Yz=n9M<3587~t^ z2dBY2AwYd|%$bMFlegjvZ9|wPhWxok-h3Y>qQ34}6Uf|`ll{OVA^d7^tnJ13dq&j| zWTT`XNS5f7-SDQnvuC*`vUqDlM9-V;Pcyxpgr{Tv`cgkWSwft7vRa$$zM;=ZjC+_28mc?%p&)&zl%-@-9zXFNM?jeL==*NF%10^I`5^VRd53BmJYW`eG_!1^6 zp|A!q_`Q4ksZMCE=xAtus42ZJ!|PrPB+$#*uFTg z-n45v)=)cS>+Qrgh(qaU(;KZZIW0=VVj}h$!*g|{XOAq(uR&5|SiTec`pBn2bGp(N zjOp7aUc~UFz_2Lp=rB)`qmZ-^{XM8FJqQIe8dw&}zuH$LW8;tdS(g!l zzeYZTBUlA+#h^gFsgXLxrNC^S!<`%CWp2U_oSk=l`*lkbqh`%23Cpa8@;383wPkNk zAXRL?6H-Cd=eaM-sAjKZIhh0xItWpG%NpKe@}*)G-0=oE_x|dLo^V z6B@_&;&TVv5(3@}3{2_e-eQX=>vkC{;$qf@9clKSt4`NBBFj{jbEj z*cp0&-uU#@XmLGiA>R)PNxK+3)OAsxJMl)DYh{HIb@B@mso4|;Kr&FG%aC+y_ku3T zT&dVX)8;6@B@PEIg6G)3#SVuYc{&MEEp~2qRL;M68=isvrW!qb!=wK+S`%AL9?;>~ zmrd_*G|wAVN2TLKs03#~lS(*lm$lN@t%CLsECGc0e($Wul(JUD83T25 z((#t$>x&CeCmfvx9b3nX4%}9x1$OhD^Cm8NUASFPV+d&N$-!up)f<$m0E;s8BBJyS zJJm&XN$a{I^so#$P(z&k{-H48nEb_(ycXby{)hgnc)s_da^z%%7*wkcO@Z2y=qy=C z_)?7`T{@q<(9Wi58J?-}g+b+Tv+`y7tP`)3B*|)ovTT5b zyk#hyIo&S29oS#?RLyEflEv{OpFZ6bpG5j->KbnYrC<$_D_=9}u|+A`cgG>LJz-6` z=CE&H*7%c|u0F>z25G%~e#lr;9QrfQeQeEEMj;>f+x{(>Kyxx4OLf;pz-vlZOTk&d zQyYJY#iA0oakcL&VV4qJ!>1FxI<{V}w2?gl{xpF)QUO#~-rOjPetcj-KEU~kE)Vz3 zMnR`5W1hYa(L*BB<*VcO0d%4NZg12W)G zR?+I80^Mq0)#SP2Xijf`LCfCRhPktaaH=hoo; zF|uollgEt=m&5b-uUv!u(JtSVCQnb(LGc{-KU`D0aqsDK;~IeO}>1-PikFp-ly@IO^7y zA1=Z8cjIiE3P0#1{1WeXz0Pxm`eIkI@mFaj{L=1sZ#EU4z{YeEzdqjY(%g-QZYog6 zQ@@w~aLIBvZnvrMHJe#p4rH9F5d>5Kg%_2IJe z@g_HSU4cZ(d}1t;oFggJTehOgc4sp@4J_?#bQxrbu7;0ql|o-utAfutsi!VCkWTOP z={u6=L64Y+cU4+>sVl=~L)Bm~n=xKGHg{j>c%D7Bq{RN1p$C&0%essciP8qQ z-tb-nTt4GJ8bo%@IxgGIG_4jLJpa>TyhCVn-@{HsM;GR2N zBK-SxP2$ZD2246$kO)Oo_g!E$`v~|3-6(yu${-!0&!e75IN9`~S>zk%=**Lut@&Tf z2j?xVUcTP-*maI9Ld~3>8Xp)F`nzvcUB8~)(R=+7pJS8oTH`<=0i2KmSImJcxIj}B z%xxJ9VQOqEU*hzES_{U}K;BG{$8Gi6&YkNsYJlF^XIDOIxsaz%ZcYi-#6N&ve0szG z=?&%!#op`j>Q(ijck!{IH@D&2miNW{T|bhzThyhs4!6&Zh<}zwb>DI%b>9Xu^5eKm zm~m$Nju}Tu*h^nzp$+S)*o)h7_MGq%|6EO=dEc%d^;L4`Qt+ryaF4@x+^Jtq{^66& zo!+K+DN=47M)}~m&99mli*E@T!TDTx4xuRr>rm2z^#hfv5cZ(itpcX~$TcUcPpPmc zIWtr{p@wSQiY9wrpLp@w+_x2nme2vCJWicQZ_)QwK6304W(3vDBTNMD5wI$$bW$36 zia$AWqh+%E^8x!Il5_l`{A$NFzBT^=AD*u^^Tz%n!xBl8Vb;>m!3H6XV1ji*D~*w-Qkg_4Yb5EU5 zvc1kqzI!q|WE^44Zxu@jrcYgzZH#*(ODST%ZxX(TO(o!IquFE?rKK?J%@_Ys)t$4Q zaW>JZjgh4{u_e+LhsFi@d&cq5Vz;5^5`8~cv*(=s-*}Obyqxp_n^DWeJ-BMFw8g^F)6>My(-vjAT`X04m zE@rH7_EUc4WvwgD__+JqxXO>d^ruM3hdjM@v1C*?$r$GEqL8_<8k9FQXsABCX8(v4AR2{S#STmL4Eb_%SH9>K zGx(MNE<$RJ(ksS%7y!=>v5oTV+A`8p7W6QXjq?OJ7CB3FAbsYJI|xf6$kKO@i>aF3 z`>K>G6M6=n0^*exLTi`;*%XEkS)`Z58)YhtT=;%-_%e5YwY{ErVM(gAEl|5U?7_fa zD};`Z^~9CIHlCG$^VB;|EF`=U*GzSF26v2$vB3L+-Zn*70(V-{}87%{?jxVN`gPEfO*MlDsX&5 z4-iF4VSTsoT7SctwPqpxTQr8+(ux3+%So{6(n_qjo2WgXo9Nti*O5tc_+qoO+LE{+ z)9W5coPqIW_+lYggURJ<2y7DT>$g@4LAT#^odiPPGqc`{$$iGk6LrZtdqj?YROSA( zimBC4Go{#Fe`uDZwQ_&ob)Lc= z3N0^ls~~SeSI>Lby#5HcjQX^Pwjhgdm!C4MqODHe$lRlWJx}epQ%>3mzmvQ>Ev%9N zlh@wXDsa0sQ>)z?s4k^0eEs%-nybj-BbGYZpk4TOWa?XJbC?Rlbz(tyqlT&Xsxeu% zxTA#`%Fi5QEv8K6Z7f;zojyR{Oyw6&9Nw#%!;+V1Gd8?ePd5D3u386iNUw$kJ6v&S z3x9}p|5;%Q1+(T|`jj71uYdlwf*<~_?_8=t_KsN#dL{u>w%OP-Y4zXa*UquAs`V(E z0t?kryL{3XyP_HYIMQ#kPNw>*W>*-(C z?`NetL%}A48_&}2wKRv3PszwrNqQsK)xtuN{O71@4ZIvj@Zz-m$G2&!2jg3>ZRj6X z5|qb})cly=$4J*IEY+kzOO=UC^`{A^;~8ol&4f*-dr4~EiK$PlS-G1`yl4ikK8-)b zfvvS|BiNrewJ3l6?r!hva66V16js(Y<{ck=K+DEmM;#w9iNVGDbVR2+u&qVm?dL*+ zh&q>O%w4dc&aCs)3hb3x6fGCe7;jLHwUFD7ILxVmchJ)hiT|3wt#)L5yaAZjJ4Tp7vMbyFq%7E6+M_;dU%o8JTc;NhHYfKwAN5amW(o~j<Nv*3d|G4L;ktvJ9h?2IxG9XT`l;xohqnNd-~Xkt%r zyB|Ik9hi_mg4Q~2vT(lRbe@ULQ}P%bC@Jl6xw|V6^)J)WXx)%5kMg+1(_z6%2&B?H zoj*)_h(gwwCcpLZn~fb#kMQ>fpIG~fmb~Uf;~R$Dv*q?tV+wfLHhh=Q`bKCKZC>}1 z%n=EUCM}%7=<;F!rsp;^02>nmYz74o-ZstpILfBQZ}}eRi0(ygt5!cNo5=Qt zZKyYi&AN~*QCK-OC=Ggd#db5E_^WZ16im6KQ^uY*fTrI@{J8nZ>!jfnr{?ah#z!sF zDP(PWI!zEB(jM3q&u(@|^GA7m^#VM2(LY7YL*ahgDBX#ZT&QUN59of91UW}8Xy zLd&~t!Ni}p4SU8!JD>)?aL5lmZ$oHmO{V6Y`Zkuv_V)EXw=08&8*@KzVveUq6PT>{ zSm_nrw9%ep-Y>R5Ylo^Y+17*F6<-RY;);JJ{7kIwnI}kykE&%#vtweB>!~j8sV3ag zwrTpDnmr^ql9Xn*!*UTlMK&N!x20P`h*STb+kzeB#~jI$nkPk)PyVZ_a9;P!#2|-V z$D!>C{P+H6a-zHSt}v|MR4?rQC+2@z55rpWKt@RacIjn9`Fqn+8HoN2`kNd;0(i1Q z6Nv4;YY8Ev1kjU3n0nuv0si?%7}m1t|7t%B(WL~)z;B7KVKq!|U) z#;O(t8~TL@UB7tB^Amc)lB>+Bf3J_SoId%wq zAqwva3Di&u;Ex6u92{x1XO{s0_kzwy7q!#WE5 zt4$c&cnIt;eTYET2%q3Xxvc@2aCV@uxhMaBjHxxkG)Ddgz>NS?^#nY^kw9U#?fHL6 z!+1i4>=HjAgxdH4djDPhj{;zft-_V{|40I}3;{IcJ>V0fX*8^l7(!!E(O|$DoGj%3 z-T{3T2|$4_e8Ku}mB0vKLmo+=pg=R?0HlbWFoqGfhh8TFUi^E}`LFN5tq9XDCS-{0 z2`2PIGGGY7@OOU{I8q`AIQ3s=gEdEEf|w7X5JAl5U~RWVR#C7aro})UC{Z@x2%%Hh z<>?oQ)v@m=sE}9rfN{92v64W3h-4qkCkhJz0q~w=kiTE@*jXnI0={+!$M&kLbHT|0ZrWmw82rt{|$Bc zd~^>ACNyIYz=|+8%#GP0Fey+WI14Cv5S$ZO7{Jar=>7?S7j9lC^%;Nz4hx0pC>|jc z{G5XCYVL!Xmgxb|1!qXg7{(BHCy)q|H3mb;+7F8b$v*%^1g*bzgSLTyREQzW1Ni^4 zi42M9L_vqpp#c%*foqHg9ry>%2(0fq1_B;#VmQ3`APNrD2Nx)d;06}T{E)R-AO`e- z7`TmaFF3I48qDmtLok6kDS!w?Kw(sY|797Np|Bu-)eRA61TOuh81IJuuRFhD0{)k+ zV0z|+{NezP!GZogC5WJpoWLXme{gHSe1;uzzz>{<>jN%|`X~wughUVs@4yL#ox?H+ zc$kX$A>3mq^w8KBzz4)30kibKaR`$erX5ot(qF&9gdEEOH{mE@`+5I*93GTZ0a%If z54a{z#!-l&get%X#2lP7%oLQ6{Vv!+&hZh533b%~o*>loy$5z?{EzeELko0(KmQY! zypXX}01||65*AD|2Ec8^82~r!|3m~S1kVHrcN7TBsKSs(0U#nspdzNgf1?jB9xRZl zAhA;@Pax`6z!|szf7JwqIesWY3vj&u6Caq+rw%|q!~)Fk_#wD8u=t9bg_$So8h{0@ zd;@GihzCCd;9*kb2^2-__+MXk9)bleuOD#nFGVQK7Y6=IDXw8?-6it=3(a?6~fiqzX0hFti(ZK z)`PK!4TVK4`hNk#{S6jsPoZJyKt=?<>MS5O;wXh15+*Y?a2|8*_61>+`R(N}s3Yclg^1jejd6A}s(c1->2>ae(wMv#Wf2#Y*IXl6N36fp;n zMi{O4|KZpKoV4gc;6>Q`W04;V3nM`@kfmK+Qc>&mw+K?sBxU8-ED-6qv?gb;Dpb|G z^EgDa$Boi1y?rrPd>8hYCd^=CE&qOu#O_jx`u+{&=$%bk&@qZg{AtzQ^2ka0#q*u} z4|fk|tFI(L!XHH~>S%DKOCx9&2-7q9FIe8WScuqxM+$2?aFe#fiW3-|n6}sngE9sV zsq*Rvx6`>V-)(%U!YHtaydbExoS65`DcUIr%Fj9qbJ-o?LH;EBhA~RODc3}*x6E5X zd5fPejr9c8ien0QrdU4~3teQ(Z+zUN_ySo$2#ceThWQ-XPD+Y?Ty)>$6_lV|o+x8Z zHV6ARI6td2a}9%)R`WC_i48r|S3@qrZf3AtCdq(3>a}lUot&HsI>sO`N2af$T)N%N zkg(c%-g5@&U5%=0hr=k_(ez1l3n!V6LWkOCyr$1W>xx-g4||aZ*-;0_gUiijqQBO0 zP{{*ZpV8}Fx(UY!CSbYqu~9bm7_1EQ?CZI*f!9gWSH=shP4N#ksv*mE7i2MfX{)23 zPjdWh#OQLs)7I;eS|yuD-Ik>#Y;J7eUk%-`a35z*g3+9~>q zagY!(Nve%2SlAoUm@fF403Dz6gDPIZ`-2GQ^mQwqGV^{R+flce$n5s0L)9Bw!n)W4 z1F(!o1aai-qz=IhS1e$ld-=$Rg|Uv`A0AZ5)qQS zBPS2_8rb+~ksQ-gu#0&FyRkjvq@eF}(KuBm_}nf{Ua1r{e`Mlqn?5NCc7I%xZUvF} z`=)0-5qD@7CY&S+J4Mfnw|G6Er=@=S>28R6c9BW~mZ zg}ulM{n&ESUdNuD-P~EahooLBVXks!@HfdLzf`n=aXE4h8c8v8usd4zCS4G9PVSOt zV%qRlLGzN6s(4R>qw?GzT+yd5w;;Pd&0V*s<=>P?3*RAKJbivIE@msy#_1pSzD`gL z-(jAR^Cx;J37N6iax#-D$5g?W_Y8-u>(ix6iiuUPys;BpUwUIhTxGpu61iOk?!YqL z!7jAG<6J80_`6qC$X_j)#5-O5IaL(~_Dv?2-eJu4ed~xdiU5eZjeF519R6gkI{&Es zG-=XIeeQdi25|{9M&n+ysl{f>XC-NuJ|xeHTgJkff@6+P(6bo+#|nx-f{HuWniD)V z&Ua&)Kil;PcPbrUpz6Iqjen}TA$t#YYkDEPiaP@Ja;1Hmj;gcHJJO|;{*s6(pkGAT zdtQwVLTPZ^dgB7TbvP4$6YnH+EgIHdqeLWopk%$Q7%;UBZ@;f=y>;PmpR=@Mu=l_J7!IBMYZiYgn* zYJ4Ws?r^In>=9j@k=3A!S5)c3HTZTYO{H&wIPtIZcX;q;zeIWxlN5g>VL6B-ERmb| z5k@tDA5vnwuo(h&q^X)?3yl)Wr|h=(rN^|IHrZj|EjTKyomrm6L^s zN4MUx9)SYkqpG-mnB%6*SP_Q~O9)cb?Wb}Y)Bx0IbF>`$a+5Cr;trRk<*du>$I#gK z#a!aM&-33dOlDr!gOwixGbblSYaPQ5ax`}LWNf!bR_>O6H{TpD_kVr7zo+>>lcmExskr zl#t;<65Y2e7DW_eSj%MA`mb#D>by9#ye}Kd~vWb2_cD7O&7XNVM4DsV<4nq_9 zO-jCXkz>nlW9mz)<~=NcyZ6AlA>4-Rg+vq*C!Vn>v&RZPjnec2MMwNm%ZC$2+! zo%R%EvD(v0(QL_l7xTQzfsIM$H<7V5>7ve|i2Q(Gbz3>-Q}AYm|SXe-B7bjhzp zcc{$%PG^yy*HDgy53E=-doB3OYq>N`gfb4qwylM6w*~E!# zMa7?@J$4Ue!Xt_#vg;kn4Oy1ch+AATa_qw$)k-oVs*Lp zx*Sk5AnRlm$yvC-G4Q)o|3_lQ)f8lCCyz>8M<&0r^ztu>3NZ4!i6oiW(XebiOuKGZ zHO!rES1HW*n4QgnUATiOHf*DndpS8VKU5#;X!>278XS_j&%50}#Rq@C5gDs~&n+k* zc4hvSUPw@2aO?+2Ny?)Rd71fkUA}R+n1v)9MJoElK&qGkh3dQEl#@}*>xdg!0MYRn z=VbQBntX=xAud;(V6VN~NgecePBmU@~yav1h2pZbbf^->{N{!=Ow(dUzE?O#u1 z=|(5OG?z%j#g`|j&g=0^>a?%$&?HI;9aKtI3wT@ToEsW@*^uYezHg%X>;59vHLOI1 zzOmr^*ux_c{abY68^>$Za#78KA8tC!^nQ37us#eH3KZkhC~le!5@-#WBo3Pg8ua6ZA!#8 zXEsB3b?`4Iu72cv5NA&x8CD2_Hu}O|`Q3@Eo4J_c#cY|{? zigt0i^?<1@`DnVZ2yZJ}JA1d3U%mU0^(NDKy>S}$SR}=(_p;ys8kEn>;dci>O^}-XL^_`gF5wyPQO5Z7$l#Z|Ha7 z&tqE|EKJzE4|6ID(26_PsIi|M}j4r-NC2Kw3mL#@_*iH>ZQZGY)v3tgZok0vTC>rD|H8h$I(Ycln!x z5<*?NfT;hQN>yiLy4e* z6cJ^P5HwI0g4eM&kRU{I2ZiHr+k8ko4hT_%gS8V?g+v~qU_nQ5L8k~%<-xy2930RK z2y7YG-@ofbF96rrg)eh?zVht@p@@uR|}IFJGVt|Aa}-+$nOPw) zfe0YCrl4K8cK_-J3L}kzkXyYCCV;suX!T!_OAx!*A!=K&vTnX3NC?5jsw=1zu>)pP zW9Xg_NaeqV2_HaW|25tuVC(SS_wLbvk`C3`LEZ-HG@zgbPXVc>%sh%9YWI#!iRor0J$MVfmf(ub3$-> zFf9r~VVlGd;=s&Zi~xiEQ~(p?v>!wQ!EOgpL*~2xbvan#j*6(Yz@Fv)MgC*}bcCpj z!x{Wb5CS8J0^z(vA%dz;fu<3qURX^8vEx6hIM8=XAO(bQ{;yD|pc@+?0>lvZet`<| z;ui`IgnkcH4QKZ6PaQ(u9KjI(-a|+TCEo`jUN1mkk@pnpdIWln;BM&@B>8s}tN{4$ z6zsPS;0RHnRhJ+P%)g8E-;s*3{s*-L`~$okj7x-s0v*LhdIo3ZZ}kxhn+k!C{&uc} z`~`rtP$1!ewWqIcp5NG<+cI*oA&~<6IIVeT@t@Iz=+M&BAmOU>(1Vbqfd;rsj&8F} zk!#;`bl7r2Xd*mQOFo)KUL3dhKx-dH8h2KN z<{NLqon<}i&KkZFk}-B0Y-YbLh^pDQX{=Y)<0>lyi;F?OSLDoQ?YT0(F{yuuE567|ZlP33VYD$wP<^hlY_yFt z+mbN2nI>Sno$%#O{fPutcCHG!!}vHq=3%6LbyEZ9H%x6AhxU?070{yWRUeW_NJtug z2RO%{ynm`NN5n@%Zh7Mua-+=_(V&v!plfc6Vbc(mnez>$o`DH^1NJ`(*G5J+;4-;f znh?wXYrLDulw@+!UTNHbCo>d{ywZWuE3CAm66fEOI1R(kc=@= zpFiG~=gP%}W7kTfijl;t+a(zv5*XE_UZ}EHz4^~a;qYQJ0=^{T`bNIPN&?+PWXNng zjXytRJH@j`u5he4863svSLQaoC536K|H5%7q2ufVBeHOq{ZqTz>vD0_OyxoYZCHb# zHXG;T3AAs@})b z^|cI1*hkCJW@ff#%1xh^4QB{gZ=V}V5;y6agXkx@dok*YEgrcmOqFL-JX__2#taMo zd=|4u8s*?BXB)0JQk|xBO&=R7i6FXacbiVU8r;-1L=Er$<6fveHIsefRkxCt#@>|T zVnIG}JTC=4NQ>dMP`{+1V)z9XjC3I(qeo{Ml(McuN__BSTDF;3CSI1g;l)~hvr2$W zw6@=A@=sn?!IjjI_ia}D#kEB1wnDbL;w@Sn;Cxg%RF zv@6=--dv-Cp9_qT-4MJQ@QPHW(d84rPI<=W*(pY2Hdd1uzc=I5s(y<(KD)0}=y1$c zT&s;^HD540#DSQ$#3ENnKu}2~#l3(5a~`1KnFh7tCEjysj!&4unG!~qXb;FdCMHFU zli@rv^`MLyGmc+ru5(P2C8p$MC8}8_{Ox(8ALUfv(p>yn%Hay?2cLO0m3W@!%bAVO zdOvx_#m%gA$?@#wB@YCxBj^psubA(l+0#0q8U0aG&IkGLuOEpY?8krXNo3`$kQH&D zZeP1*y#PmkAVyjo_EhO*m-TRtmHRPf8f2l*EgQ@qGmeuP*-cm5k8V4JqGiEmwVvDb zy#VuPzg85qCWk$&nH)?=%e4N_#?VaR%J<`rAq$+|G$+GDBCS25=h6mV(;X3VjUo%= zg@c0Z2Nc0uAFe$D`jfiDqo&WopS*YH9!tDdSBYG3jG)>M{i?;Tw#6T1YW17C!2EYW zOiT=x!d<2c4V8YE;`x>G-2zrmC!pX0J9U`y9}ix0#ihXFUGoy~&zJG<<_MbuMa8v|i$>3GO8O!>))P15$IlVtxiL{M1gz^L)0O07~#3a-pkp zM5VSlS8tEcP1&Xd*L+Sc!4XM4jIDbi-m|&Bafb)AgJ!+3I$(nc9>~spiKY3dz{?@p zCtUYvK<#*DpW;5NUghqu>h*BG*M2_1ONEwf+?Fs66ssn`^<|Ic^KB{GLlWK;q%kaW zN$#m1PV9A{lv;Q5Z|RQjy2=;KRu7Hv2fh;Yjp&v$j@tbN{zsv6A+JL5abr@H)chDy z@|yT7X~g?wR_QU!NaY}MGu6#ygMj^7yaAu|;)M@8`Y-ed>pe_V#&Oo;ZL+kmU!;Z? zHzs%Ziw|#VYE&SrOyc+l4baSt@J3(+6h2KzQMjRa*5Zzx+M=$Gdp5HqBSbiPE`t9k7+Ck&*iqIDa=Uv4A+B+cw%1F^DxFW-ku#ijsa`LwV|Sl)2-gtBB=ApP zs1MKtAFx;KB&sZX*)oDa3B*P!9S~cceJ{(2=;te{a&^t-|Gu!{ zcxza8<#0eUch@m;US>c`(SCImwsIS?%bt^g$5JkvNa35&e_-><`CUA_Wl@BuJKqM& zcd7jqhcjpJ7ne(&DK*ehdx?nehbP1v=l-ax46JX3CL^S(mg;+A-GIY8O<6PFfI7zbh&^ufc1kh7KGF`UeK@r5^aA}u z*M=%BUZt9e_RNz9?Y3BhdtsqnOwh54$+yQVD|@pfFokbq|113#oi9>&XrfW!QdI+` zY~@8Ia$*ZVcbxg#HVe}Z3m4x1{*Tk%h(p|3A2MlcK1E>dxv z&LdTuSSrjKX!L*Xkh~4;<_ZVAWh z;fRsu5e#O01hAd&t{+kf`LGQQzUfH5WXyS+D`9vq_@i^P5-lkyv>`fFj>&tm?%`<5 z=UKYV^-L~g^+}Ea%`$y9EhT`7#+Ln6;`a-<6K{>`pHiyydM4i`K4&S%m1gD-Pb1vfu#Hb$aOsRCW ztxH7)c;&m7mQSVJgS1wOo2CP>RUhB|fvP-8s03Q6N?Z#HJq|Ui{;BMKcMBOCNvP1Q z`Bnh_Js!$j5vxo&5pLFkQVas*=^8b7=m0K z-CgTF6ReA^6KQ+n%qhI2jN<8pNlcp?3Vb=h$BK2`uO zcGtE0_+HoUI~2Rrd^^e;4M<4Tm6jWj=_OQu{ZY?G5-6ZHEB)$~@|9+2|2&uKEB*S^ zG5#IN*AD$^@_u_5De`8@S6UCx_)MHtGSX+lV+7ExsIj#`9A4kZyQcVLrQaq_1%=r- zgK2V>t>r5EQgLldhL{;#JdD1vBw|UFxnG8Vd09gxAuv&iQff?EB%;YXQceJSZW0W` zjd^NQoV{;f;Ih@eP?H*g-2NqXf^`V?By0(5`uiTs$>%aFxq#xrmc7y9k1y||6+Luf z{0>wxjoNmlByh3@jCm>b223wVVU7|sda z&G}I}*z)yO1~^pbvs&AS%hu@(4CfQKR?dEPNXaj;US{1B^#d(S!6jv!UHr{-fkNp>DI~Y7+kR)Qn z_^h|anuTOa^z?kwSUH0P-}l0Sm2^&ql8WL%mZVwO?Ki5vfe+o9T=NyheWy4xY9 znA3uMT>irT_*&J=V9^`XQV2)!Ow%hk{{sQ72qDVvX`OusEfcF^$7$GGkChi6lC-L5 zQ%`R+%Bh=Qi;+X_IFe%PlbXOhc@Bn$?jB+@%d`{9)5FBCYLs_U_Zyg#7vCUl=|M{{ z?)TNcuW0-5Y(Z0s@yG(>)QVDdh z!Z6(yo9y5*Xp~jWx9kw<*P(61AprZGZ2mvyLvjiVJow^IBGm-cY#-|KGc`te8HASa zK&+Wl)gEQe6u*7v=$s*FE%s-SeMOd|s$HLbhJpYPAtTBhOUWJjWHnXPQ|a9Xt%Uc5 zO$KHAUdj(r`0gzE_1)88onK>3M}GM|hIjvX-&b$7ZtLS^nzbTpN$!AarkeM~%Kqdu zDAIzY3LKJN0Tg^yw^NM8S zR?9-uqBTt_bYoL`1Pb1#B#MVdo^(zwEU@|Y% zpLqDfOV`afQX#KG@vaA{6t!qq^CbsY)+u<*vLuOeO$~dd_a+ZJMLd%;UYjqDl<;C5 zwKe)ozO)`Vm|K5uzW~V4pJs7xq!3A>F#9Q~?VUD~@w}k&6*pMqT>-~=$=)+wOr7oY z%fr?mosMn$_9Q)1jkFQW@3fqIo(V^z5_#E;{tN!^n>`Onzj>E(FP87701Y1#=PXP#E_8C=eoEV7GY8O|aVNuSVGe<^fxyZou!o#QU2?tC}xd`0!0{$hJ} z@R89YQDl z93{5ztDoBimTAU)3Z-#5BzW`vp2n-(x#2B3 z&Q;s>={_W{UyeD>W%B-(v?;WA9VYarn0?w8{Ti(M`5R_?Dc-*=`>lY^&MF^^J3k*9^=(_@ zHG7ILL~mAiPOG|mM=~OsuHxB1_EKt-v|GvP?2(mbB$HyWD%K<9lDe-;fggv?Dk!Zw z<{Ai|VRe#cd>)UTB)U^nA!e^uvV7buXPM#`s?(TolL-<5HI(AZhMbniCln>QWOcOrgHQt4j7e=M5Z=Tlwo z_~}^1!Ex9GkJJ~Kk_EGx%f$P7DXH%a9(qi`&95ARY72gG6?buTi_9JROk&6q<`ls{ zJ2kR0be$&)d}@937MzMPl0xr3Pjf1X)Lr4n^D;gEuvR1d@o4B;eTA+}bJz-Sv?%KT zv@TK<~Lq#o~&SkirN$IS?W=TnC<1ih366&q^+{bk`UrtjN`cxtr_?%yaIG&;v zOnX(RK=X6R>WJR!- zUWaxF*x5hv!7?g98W@~;yx|qsCdSheGJF9Zo0vK63iMb=+WQ82!N=OWc6k!!jg#i( zsZ$WS$bM|gV_I9(ELK2DXBkyGUOArr7Q809K8$-88Ypkmn=|wn4D4MiBBAtZ>nr*k z_+8PPUTD}Pst|otS!vw;a9@4m(u`_h+(ifYx=2}wdEamsSi&~i9gk!M`Gj; zDi5-5+U@=fw23T5UTw`CV9ogPYwc@xyOU+GcxTBdGWE0C_JQgubjpcJNWb)irwTYV zL>(RWt6y>hqkLHj(KVqP;nTCXNs(`&N|`+P&ght#uekhs4$`cqgl_8&(f5t6s4B#- zT7R{_ei4DW+$hAyHaz!K{}VCrXh%hFLjf9~-lpI>t&p{$OsVT@TWo)4;JYYU>9mHauY zaiYp!{3(KBudAH@-3Vjqq~wK=h5<;&sbHW`~Ely zSeJ^aD*KdS!-G*+Fmh9GFMgMPU&-V3Kf}3nyVm5&g&(`L&OF6AvNh|BB?pUC2o@;r zUjYq%6t(UOWfGUXPwgFiNoH+MFZ1$H#qC{DsUmVp=*eVftzsz_C|KA(Lsk4P`keyy z|7q(w;HmunKNt7fk+Ns@-g}0KP*y_}Q9_}Fj9elk+f`55tBj0{>{SR2A)@S&QFbIU z{^z;(ru@EtuUD_0=kxxY&pPLP&bjBg_xU7tW~wN93nwe0gl_zdr#f`ePK9&_y(@Ix zkjSbsMP+kUVKx!2Q2w-(q<_>v#@R}pw{HlXL; zHFO$JchKnPaW^b&wLOLklAj$Zt0_e@bJ@<)s$_NFsu<~7H>xPI(bUwp;kJspP~?!t zl0E9}WXwHeoKz|*-qH3W%cpx{A&XFPHYIaqf@qnljEiN`FdyQ9z*UTs%SHlTI(0P`_ikA2(#;{0uXf@2FVKtT@oPOV$P-_`#sRi@yHr zhM`8mvk2l-v6|d76=|9dLQ-x0ASxO31SILqG4P zMIJjpdxLRkYJ5(#r!Ag?A#^}<;L)e9!zSJ-vrJ!N7ag)7q7}v#}^0PJqT3Y83LLe0z|S1)Wk_3u|TB@$Iji7Wz*~Hbghnh-L=-3fPUfs zj3t+RsOO_$S+Mg{;aPg$n#hGG1G9eB81{^s8OiFz@=n!Nm4$l!b#S_|B1pV?7`!#T zOUojMUu!KSGutzQhxA2CFk?GSUYO(u8IuUJcZ4@3yCaO471o)PBX30xtGnfK2|&k3 zn5~SZCOx00BZokxTjPhc8shAkHR!;59^2sO6bBw9=Cl>tEj+n}DW5@~+&zNMxE!MA z!d4zqk|y`tU-Sl_>ZOv_Lanlp&Bt>i+~}w&iN;LpTz@04$5^li#a@r?*S9dv8Pk7Tk|`zx|5V_i|-!$OxQ zUhWbm>@HL9-g&Jt;abDN9^@6SSa@wxMBr;|XN?V&N(O14@xZKEm4cAGs-4Bpx~HEW zu#||YiH%@Bw%MlTs}u)Nd{uf8kd2mKdKa_PkU;f9-ND0y=mW(MaSk)O>w#OT!LsT^ zoxb#)E3DQNUI_-`onfm--lxyI9#J0ExRBPedXQk_vc|4(!l%r1UQ90k2U%o#yk0A) zXLTV!sxvir_F{Y3muS9^M`kSOZ+{sgJXPaYo_Qv@z5hg$0sE!zz0S<#=OtR((2`NE zmFjQD{4BUr$qOF@@P@P8b|5-)kGJcX)9&}QnFDDvRGsU5PrAR;#_2V^ySetRJL&O8 z1a~A?lzx4-EXUD^ru7Vi_p9TqAzxM9Sfh&CqTfm`Z85!W%g0;xzVK}=G%sk z+KFrmwb8F`4L|W?g-}nhir_0pQrN8rleoGHsxdedP&(AeQ-`>XNR!nTDP}{yAZxetF~P3ejQ#EP-R`eBKC1J*`N>hAnsS>N zZ9@rky2B(|#QYy#4CJ+qys@JDVaD?P?bsEIoE?Stzg~z<6v@1H%I+i+o28JvAc{cK zJ|1#$sTjGo>eQDy`e|{V(M4WzSWIs8hqz3O*fw#WUHH`_Q*O1A?X+F7GggSqW3)%A z#UF-7xg6CdAdsnQ2tc#E^d!8y^=nSU*Yu$}{mT_@nqLG9?Qd`NgsJv~OgJd`I2`(Y zRqA){Bf6`|^S7i@NEG!-Zy$WBl(eZVl7dcWI!}7~R(h_OEki)@y@cX$%hw$hvdj@e zVb=M?BOWfV&enV)b1tA!#^~MqT$OmAP0Z-^sHtZ8v-@3?lbzFI3}aH#Y|w1`FyjjA z`HghN#Er47%2O|GZ4;Uksq=>Tcu}8dEayAf`@$RV%TM_b4eR*HjD+;J*4&d9E)MWV z^N)V~>TVRDH2vYm+{D%MrE}k)7|#ISr6AVM|*sg&PSpp?sF#ITe{S8>NDRUgTZ%tegZ~|Xra=T*h7kMl@>i`FoUuml60q2H;u2mUKSjl z;%Rc!!+X%UbIxD)gXyi`=lpEg&ISCk|8)NBP-(yx};PT4}8Md-)3VN}(cC$*S$tYNh|Rv6~rjlY54<>Z=Nc+p7~2&yD#OZ_ZpK-So+AV%<~^c|f||>bM!A z+l}7+K(l>OHhtHrb8$?=H+0J-0n@-ZdB%T>m$TvEq&U%rIcGzH&dPn#l~Svf-^&p7 z#!yuoS3~KEono<#*6K-)?{VsY&rx%yxN9<6X2U6Dt7Yl*;nxR4wpu0QM7)q!SA0U+ z87`@w-|*p*Q>C`sVbr_g>Cn8!>Bifp!iKghyjylUFhW|^D|1=pv6AxS6RpexC1E zL@D;Si#nzR9LQES2E_b#{Mc}=4Fc~;V2N`fQw z=&uYT)onW&grZzAOCs0L>3&3z9rU&!eCshD7NFf4Hmyfyq@peulZE=ma{QIXMti{I zEp+2Gv#X;XrT6`{PnT$mI&irTs>3+07MSpBU>Jenhg8fG7pdLjQu|dCrcPIr< z4aO1^U_n@)3rjW*QjQ~0eyWpdA0f3XWg8Z1Yu*jy$b&tA#Ne9JVx zd@WlhKgqn>z`T%ly;@vwMMi9V?Y$j{-%{?n-U-WS z?}W}Rf9Re`Sgvn}%!C&e>-YL^8kT1_8X7la7>iYk0>&@;pPJVE*23_T!OEb@mn`z@ z3EBlq9bR9>2bac5BhXrFzP!mTXSyPNWrsTYI+67c<(A!7c`inG+KZ~Tey7>Y&yL>U z+kEIUrBHwGtBOQUij5@|^exZ-oCq{#xr^t-7mTnM=eMMEVx?RS)Fl);>S z@s#Z^UfIVNPq)+_d$6?F@<@>+rZ%A3p~>*6VSLV*+&$;4CfZwvZI1cS8_ML-o<3I= zfxg20_J%m&m}Pk};!5(X*`?g>BTVUrkBx1Qdr#O_59U1ZK6|d4Fu1ct zX_ft3dP6Y3IO{?!!M)-y3R*FwPo#O7o}92}LdO}!bsw*j_?dfY;+jfirk!lN0!nmF z!WzwP%T#04sK4O{mH4^O?=hjrqC2%*&L4M~KH5vtIsD|p?-_-a$6KY^eoCua zvFl&bkz*K-`E+7mdd@r*B($TW z*f`M5i%*?@(}?kub-y4kr00{Mq;l5;PWZ8)J z#sk^*N8_qub{w{o=Vl*LKHzp8Dhs5sK+`#2PhU&C`^ox_ik8Cr8=oTr-d?$8`N@aG z$}U<1JyG)c5M;>xA#oux?pt#FlWZmqO()E~Hi5FAmlBkZbZt5@lTrTWJ~eusvr}2) zyE>uyFiA=+ipPuN^d{kHDvnDwo3x@Wcds1Yef)`&u`X#>vu`DPRbuyn+>ep8L+EVC zi=nP1wM%(Rv95KuS!(&jW^BVHarAqp#vZ~gsu=X<`Hr#0+dR7j@jJ1tg!U&#Q-5g> zzdu|X;}-&=ba_#(aWnz2w8lRm#s6m&=Xj$7@Djp?!;;tG&*C0D#pxO zpo;0x4v~TVE7N5qGMBP1>0QV_pJh#p&eQ3z+l&?K4i!a57aOf7wK19H(JGT&a0xB6 z52%o@3B3~;-VarU*1b78H&OLT=bCGM^I7hzMfZ~*Z}t!b9qLYnQZomxHBT|jPMy-u ze`7T#hAN5r*l)FP$+Iea=!fN83RA#PN!!SjHQMujQ@GpFXv5E7u%~2OR}8h1h+SHD zrA;h7uQ7Kqrzq_y)#r9mvj@!cO>^v|<5uq$gdSag@yV04EV}HdvSlRCxx|;$7K=w| zcIiIR$X&X4+;8Kn1OJN#D&GfRr^zgXE?>L(P|l;sQgfuHf25{uy-ei#B-y$0$hQ{q zlFD}d=nIl_8FNb)9Qb%n3ejoX8)v+ED&tPPW>(d6hAN%a{SZ}?$m?7i89g?-Vj49r z9?y8~6iTm?H|bw_z6bS4jdGSu_MU(lKSLi-IQj_MSu&1WnVm^IEFEa0BV_yzZfmFa|1H&?N zA!4FB-Tf!zQNyM8Wu_`Ah<5Bg;*p83cL`-8E3*gDS7-CUX_=}69$B(^BFm=38gX_a zb>l|9=)_*y4v`kRp;_;Y;xBYnnN;_bOKQrpP6(I0or!7|obT6|kZRGQZeM5is=t*w zU|K$aIqCh%_jlXo>CMmi!HQPDI$NA#sVCTi6tk=+^8BlhguXMjofVLc3Vyb1md4k) z?06(OCn_7$!()+0?og!^&~X|wdr^@5)EdZ#-X~O}XYJP|$vuNdIQnaVC3-*j*^q|GSKA>Ph45WrX>g@Lq&MIA+7Q zr{=8axKq!idM)!MofknIXHJX>2>m)d=%Z!6%+Y&u=TyNcI*(Z)+~TXbtdL%T|J$>< zku%DSZ_uUjV+z5B@w1hIi8Hq+4zfKOA#EyQYvPq@tu!Or8S~#bQAXE)0#Y*Kd|>36 zq=6RlOln*smSje*Yn zWu0(i-KjvIGYo?@)uSnPmQP=*hBUbTB#atwU_nFQm;4lV&k+U9BEQQG{aAhVl^EEaVXkDrPpONcQbd#h~25Glx#?vE-i)ji?Ct)JJMt$8cT|S zN;Rgmj?jMeLat}vBwF=_j?U8{pKq$1%Bah6Zv;sBO^cdEsLuo@ovRIKjVoedexUny zE-Uyu>m^P-#lf_{oGX<1sQB7vs%jk^3#)ht$RwLOs_n3-NZ%o2K3Hp zRKG~J(5<|(K7fk<OKWjn*}G6vn7J}u@~)~=duc>Ls58yyNIxcKC23z>ej z*h94V7dLOYQ&Nm1VkF~)BQ0D-PFDg>5V~d7k-OS?*`BWMqvtDXL`V$zBxFr7_ne)- zQe1Z2b+EnBzslUO@h0-5SEgO=%gH2v`%qg9R z#V}@u-x2@*K!@qdEcpVf66L$9gO;`AuWAxZ(7m!W!)rZSLmGE_ESPhgvtBx?vgJBw zsJKffwTp(jI%@^FDI1!5zDyr9{L!mgW-xTZK8Na7N_DiD$@)jCZxy#{y#3|ItB%HX%*Dnlrjz<(NO5uyOV1^ zZuJdqJIJ{~uN2rJ|A|@ms8vU1X_?Z-?2$ZqXYB!E3ylgVOw7A-e&Y861*L9$0#=3T zUP7`8wgws5Ns^SOEe@;B_(UToZ!3kdcQ;#U9j&$ph7(VH15 zFD6HH1Ba5r&e|O%JyA}xZ9sUG<)fF#Bbzfc{^(d+hJ%X>Pty5{X#8huP`9;CE@sj? zN00G&?A8n=rzBd>W%d+zcx(t5SW0~fr~UkzVS47dkSNO7Twvn)1*4PKP538onS5mK zS+(zxQRL}KGwLR^XULvmzjT(rm)rTuT3gwCmht<6N8&S2{IxN;mhZC#&7KyOkF${^ z@~$00*S31RIi>7RubkW9eXpc3*Yv5Np z^mkvKj~rO-*tk@nk#!>Kk#R}OT)NbJD|hK?_;q6k!ZnCSg(xSA_e6Q_Q^KB_R{;(i zZFfWZ&3>>&4O2(abG|P5aPf&!wl6)IZnm96R0q2KV3g9BlW}>JS>5NGgCghEIRaJ+ z$CF&k5|ili>HX*JI4%4lpe8Egdw z=QNxO+l)6ljebJ%O?o}1mnPqPk${rL?8tf6VAhM&r_!logARwY%6VT6xN@#Pw_kL+ zYFcI-?H8-wT0ymI7ZT`Us5xuzU;d%;W98d`A^Wa_cb^U=aZHPy8eOa>kbGj1tyX;E zr{JRq_V6F?G-`uCs3=PrtqBL$)ypLq-3*j#5wj%s~n{Jkuh*Fb); z`KG+sA=aD|#hkilU8|m@p8295< zi;S4pHrG)qkgD!x2IU%Th-dh?15+fuJHJ=%mUo2@AFGKTQ`@y!b?>c+DGb`&q0Uo+ zIwaZUdY?S4)_>*_ zaMjw3ZC&z9I8v-tomy0wd@}Y~jcEC5MgPGLVG3GJGgGgx&8XC=L64KOD&#GG?`{vC zzBVB!VUDI=*`aLNk?+}{E>OMrg}0zT<)P`R8xiK30_+b(;^k?wOAU2WI0765T5ec1 zVir7QcAdk5SbFQcPIIOo-Ik+iNcK*5tTmOTYFv!Ar2bs(s9O^_o#jIm*1ZsLOZe&i zxASJXOKB+?3tUo%Cob2aN?f0LUo~nR%x8BloDNJ!Pe1PbAu?0h%%&*wTH!7qv#v?3 zWRQ^#L3J~uzHo$!g#)V=15*=Wh1ab_$-C?lI$hzfi#z9OSLHMP)wH&YB8my#FI#*- z6X}Sjm!{iObGAY$kDi?|vNhdUwu_Co5&fFNvl9i>>NxRPO-Wck=6c z>Q2I)xtDh$lQ>wA+d`e+w+Lv=hh{~BJnvEe92m=;86nocwp^9Fa3$zm#;>vlNAljX z8*AxVr=9Mn&jcZc0?_5@SKod?OG{TXz5OmS&*7ZiiT=1VHdlL$e>uAN{Fk3|VYlZJ zR!t|Ty)Vr2G=K2urVNo6BIDw4ITcx{(wkOp_;O9rOHs3s>f1d*iNyJ_YZVuISok%1 zz>6y%X*z@=iF72_WS<`6;j;6*!qPrgaI5;w?Js0GYsFCg(=T_eH>|XCJ)Um~ALTsf zekl!_H|_F3JIs50`xd_%tF)e++f|Z7GMFGpaL!QpSG z^VM12j^+5)OVahc&$wkPcbS>LEB3YJqYV{i`y_*w*MgxTC-23kOr~@+&As@+Wu-5f z(iQ9;<;E(2(Jx~fFdBEf&%vWCq$N?3>+yw5Nc{3RhcY^J-qEfySYC&Ue!}quU4eap zM~P_-)kc#>)D^SY0@92K0}<)fH{x{H?$WnSg(SU?cuW36zhUkwadCzfX>>8;^Ru++ zButyl8MT5jS5<^Vwq>@%Lh0s8eMx@yujl?wE*3u~Z8KPD5ZQ6+o^K7Yxq{TkW7i7E z>z2XWD?3RpqFtPRUPxApznbr_;>7vEg4w#K>#XguW*?6UnZas@;@$2?2rs7z#R)Hd zwx0=ZmDjF5RXyM{rY!N%YJ947_UyY-^Ez368mO0h^reMoskd^sd;bpw3Kb=m!U@Z7 zkvX2BH|lHr=Z&l)8E*A=Cz>Q%5(N`mkBn!vYEty4cOcNGQ*=TEx?967xI>iAp=wSG zXiJ7_g-lfhu4m_%CeQRf8_D3@RJeJ*{mwDRM}AP}9d~Y;KJFnmi!0Tw%l*nnca)=c zJ|&bgXW+kD^ZQ8bC$ivQxUsOrGovHrwIldMGTT)Lmvu||k}Bxm^I zN&3x_5i?42bZwy2oC9&KapnlkgrCYnDqYo5iR#8$%nz!bmde;omhUqPmi&zJFZ+h| zF_8n--zrR>+(0Ee59%>kZ&cHVXRn3IyR>S~5Q^MPd39-uDIby2$~<~q`{^R{fsd%y zI_vqK{v|Jpes<29InMb@{>nM1T2JgB!`s_xF`9LPYYgo-N4{<=zrIoabM}VJ<3iaB zpSBMZo%YmM`F?+RVg6W!JZ;_3&AQUB$}!fSR!Q%=obx5tO)`FkW`8A>r@31}_3by^ zp|(#^XVTa>@}gPa`nI=UjR@+97VQYQG8uFsC9rB^PHV%<+Gmh$(`s-&KK^qfODS`2 z>-QrfQr!pHbHWDqV&3mI5=90t0{Hmi=ZX9^^ir0yPS&C(& zOJu7rO)VUWnQnKg^*0~&sBB>C`FvXuvFgp@Bs-%w>z<4jD!w=(yTwP)|10o!T+KF; zUd3_C)J&pvCQB_{*JMzURimuQQj0I<(#Rpwi;?A;FlQ!ir-% zKK94Iy?E#$Z@+%3tQY0e()!Nzio!`lzgl79b5hqUgk#llK_}jpWgY1@kZ@!~pA)>J%;0z|O8@;sOY`9P2j^l) z-XGVSf2FMg>6cErOs6`lS*DECdTp<_nMo7HTsE3AW2Aq4{z3vj&wT=?++K!<76=*r zALFhz*S#L{kXT7EeT;ibK`)%le}+R0vp7zdw(Z{&J#f>*{&wm07Zxqgrk$HgF_@2& z({DZd2-4AMw9spl$0Z9MO&=l>?_`@evblL=@Y@P67)+tKE5w4=n{u_}1kGMI+ke>2 zO-$yglWfS(O3+ijPMS1nVgB`)oS`{w;(%<)ow%OBd8s_n)Q>%{tV{ExKZ#KaNvN*z zh}85CA!wwQY}yMoVm>Mu>1$9Lr0~>{f$cxWM?w>=9fW>Zc2I6p6IxX>RP{E>j1Zl$ zS5JC+?x)HSC-qs(a5w9rtDE;6wXGU99rP&Vw&&FYg<1lRUHtGRoqoMfZ~Wo!WG3m{ zDPD54_xvoM%h(6|tVGL(9g4@ohI4mg)ti_|{ol%plAB#Vdig?El3|<}&4KdnOZV?z z(lEX0%z*9{L2~g0NS95noor2Lc6_bjRhh(7DgE4RDPFJ1Kv%;ihs_}xO(b*S!eET2 zADQA^h<~V5?rlSC<>OP*x|S;otyd($eto_koG}OCs8c52>3k{ zR?zxAteY~}IvnLrVt$|e+6>>k-z@^PsGzIIi;*At^rI1N-ShLLAATPvT1JF#b=>$| zUH3vGwH3|ypy+(t!dZ)STDf)p-xR^q+sh0zUk&QefrNa&`j3;ORj0*r!jHbqma?UA>l1Gv3IVd-bg>TvmHl9kfk z_`&ns%M`tmV^m6*#PuUg)r=DrB!RPMtetbFblQ*W74y;5#N2hDv~0Um&xX0{RABjt zQd;ch{dV;GyNP2&rOM9vTsnk0GY2_BttZDRWawgBw6go#G3djftg3Ob{e7e z#uWx?T!x{A~zLCZEW*v1GOyP32aHTjqK-nEc= zopG~RPQ@L(X0SziN`dH-M9_p-oSTewQ{rx3)eD2T!$HTj&CB!W&bzX=PHu;=j2F6j zu$Qe2T9k!+W=daRI7mTCel~D-Rszwa!N=6=Wa6}5Y)cd*sz7fohdExfJ}siGQ#@eY z>*^)5%#RPmgkSet&@6>f49H|AH< ztQkqv;cet+8G2}LOr-Nn2%L#=5Y*rrnv_7ShRvsQLHXwbG@B@1T4sdJ<=9pG3)GIG zo*lMhEU|kQbHj^NI(V$U*pl=vwTZ~%FsswmRX@<^<_(+G`Ky6h{N(wy93kuFTMQd- zPX{O)7e_ZFR?az!P=8r*(JY&>z>-!cyleM}9x2u#H;JvmGT;ka=*Km-y|ao4=0Qg(&!cI{1Ph*)q6` z@HWBo3LSKJw@ADyJ0#_^#39$)uT8=UTQd;y2=)_8OhhG23N{w6 zhR#V%_vp^6tH~-#ZnWLx8}eHWz4N4nXYGCtpF-Xn%F``ZjwZbN_)z+tXs~8|qhM_I z=nm_I$xT1^qqP?mb%ZWkW0arj#&My7RqxRS9sE2kiPn<~pQ?F}@Xjq1MGTNXoN+?j zrw`!1cU*)?ti0;U-4pa7-sh~!S+p?`7=+xl)^mIXXYLF(sWmUDO*Yna1iD)^I?6kp zsgsqt$JlC__l`u@G;I*^RfNc=GnEMyAnAPe;l20<`LdSC8uG0t(FqqfIMp;5lbAzf z0%^8>@|K`~2l|q2efmx@TSEEP{;rpa?`*BklBn{}CZBn7hLO9TeNi`RuO9utwRy-j zu`yUvL+o(#q8*=^cQ}##OJ}4LnMeH4%LC7^i_tnrKbfbN%`%qs3M{U7C)47kCG3Bg zlgRq|#m3w5?AAK6^qZwE`V$|ms8dWH{SwqbWhC|xXrX^pU7>U%bor6=_+BH)Nw$bK z51y9`cc+Wz{Dd4eCUOdv_#MS14t-=f9!RLCbA^*3STE;GRHjWeLi{P&Q{m_0#7l}w zC~7?if-f1X7-`4Te9=mO1CD_KudOb^-U3F_04F-dq>2)4wM+nS05U*q~+jeC32D=j25{vIN&x8mH7i<2Ur)#b8__&ibK@fkBZ|@$Gl&s8VUbtaHZw!XH`45 zn@pH+t7}rd?=GiLwuPs6#qqZ(PV-oLkm@?-F$yZ|T;V|HAV+Rkow*gs5Hy?_`zv3L z?@YaNf}o3pRBI8F-LVN{gqlzAiWZlz@6f8=&#*Dn(c723NQwELy;311cv#C${8FNB zQjloPaQ57yb?bP!N5mDO5 z&fiT*t{TidjOLe;^W+jcTVMC)pmp9a-|bLyvQ)~Qm4xQZ%VlfFhwKbBo!&GAJbp!5 z(KolOwLVaAk@MbLR6+q$>q)sQQAsC+92Qt72znodGO7)h<;7;zH!6t9j0}j%);vj| zP?9dr6rRf4S!t$RD@mLmO8H3>&qsxl@|mBHyyEe8{8lMLPhnr<1e=Ers-mAYNmB-gmF6Xhqbh=gNs9BZR>^a>?(YZU;pHN zTrfoZ`sJg6vdGV9wkqB8zB-?r1=Zx0`;#ws-^vq94rrZyS;nyNg>5>+wg5F%PSo^! zz1<<^26Tz6I_d>E-M4BAUMcaNpY$279`{*NtL?wJNS!WIPi?5V9m!F9uuZG-s{W`e z#mDnng(@30TpWUi7n%;cdB!KkJHF#fZDx6IAu@63*yUM+v~l#j1l_Me7fS0Ra}-k2 zI%i)?mYVlxiCR8Z`!alGL2c}k(tNX~3A4VwNF8skiBh2U;0<;k)FZ2xf=Ol`f%TiN zVNclZdzoE->-)Rj!Q7?qmM@=cyBW>V|b)3=f!xNQqS=wj>N5HKAAe)pz2hQ zOk&@nfL}T_^I665@E8RF!8_4^K35RXIWj^Y?h?l^sdAranv6d*VJ8OOgpVTiyes}j zpMZeCm;kzQn}iiwpeCV#tW${(gp>vngMTCyrGdyvG9pP2MkpZ?serxcjYO)Go=7}s z#r*Bab%-?zDT962jA;~79D8vu3W<9i5BTDmDweVvh1A4eXh$Q@V=pqIk{zAG>zQi%x& zKu`ShcT@sejzMxmZ!kzktPuYPK*&2OAek46Nd$&yJV74BBEv8;ECUs%B84&j#Yjjk z504yu2#_3Q%u8#bI3*+$^$>}D0}zx9)BRNU48nX+J_NnX#-R@(0FrA!w0TH=%tbYF z$OytWvx1SwTA&#dX7nfv)LDTC>R_Pe7#7fALqgGcNYukeNPet&LXTkZH~@Ju)VhcN zwu*s)Z~Cy^641~iV4;cOl7%sWbaB6Fejgu&p+>Shm zzI+hUjfb!AUY!ZIL&WDgsDg+ ztP-_UII!-(CN@m184PNE54zJOLAtA`8h#3JdlZM2R@YI$)PE zh{%b3th$f(;tS^hOyWcSziLynfZ98Wdr^SXAPNe7h@Vuy0ZiBj-FSjz#&CQh#Yj-$ zavV-&dx_>2=y(~>t>XJs&ZX`N1ZfO6NwH1lJAbR9N8xZkqssTJI3e<5~3tR;WBF#EJm*gmJcmH zMRHn0>$0aY0 z9Hbmp=j%FsS3eLiQ{esn%)ws0m*;TN$MU~>C?|XzDD_1W5J>D(x>d6mEx1pi>q$sP zY?GyL4x^%=$roVf$$d>S>fyyp40EcM8{7R@_G*0-SvU$i;Ggt9c7vB7kYbSQ8~lvZ z(LFQ-7vV_;w(rlR=U>6gk~&tN!+;82vagsAWUzF@IRax{3u=zU;)N%}fL70+g$1|tVtuj}(r$PTeEDw%x9x-1I&CsI zSs7bh$jFEMpCJz;Jo{mfprQi&>UkF(ccBY(y9x;1{;3)88M~rGj)h1mY(!EEffa8D zu%Qwkq=Psx_+`ZCBV)or5LiDD*!@GnX&80}8zro{aMS@K_Kyw!KkmPQQ6TpaTy2R$ z_C4^08+MiG(|8Z1I`alwxMTaig^pL|zlLQDWT9jrhEvU-kBY}cLxmy}r{ z$qFRnf&Wnp*^r}H5CK?_4f^({$p+ly9i9rxI32+{c86i$VGr=^1nA}cE9p-TSUMYA z&xz&l)}po;ND<@SgQy{LY7}m-z*GVt5q=zG3U08$zX4O3g<i4a1dNU3IY|R zNTI=l_=KAaj;)T zm3>3Z8H%kyVxO=;BNaeNm^DrbdnFS0qEXdKKyA0hQ9*B!!S$xtv2i?o{S|*Yn1Z=L z>i!g7?(h%)LM1*`9H;`K8l7QC2&y@ZA39DjRq7T@6^7X8QMeBR9WYhqHcpKHZ9-az zy&4~L(P{u~d*Q@@8dbRECkGVAC<3m*iFf~CKIDs&0*Xq}V^bWqH-5uX`0wB#Flsok zRLdGbT@1oe6Kjxs*ziRKQ!ai6j@E&V$Nm@-L8CSJ7%x5NJGcXw3aS2gzpDxztwm~K zLzY7xSI7){Y7KP8{{2VtgFREIpvz3CLs)yh)dG8%AK{=sHLh?GI#mZCsRXF234DNx zsl%tW)ob)^<3P9oFjakDPX}UId7x>I0L|Kgzj70wE)27oW6 z!nhzLVGF9g)HL8nZYK=2JjFEyb!Cn~J7DXL^_9wasyG`=vG79#1giT?8GeT2hf8Js z6;;>>Ybn7&pkfv$b{TkgSc|y|;`#uVfu9vP4Ah1>1Q(~m8R!2JG-IuTAz8Q@5g7!E zXHh|mt#~uI&f1bpfrwlK5i#6n##$Xt9$a1PFaPJ*Ma2xzk6fT1_eb#bE0`Y}frEcP zCXKuT`Y*S_5I0=^h!^X;%E=>lN5Ik&2b|w8z@Ks+*mhdE*qJY;oR)#fEf(2 z{m<15gfD);wE%u&YYwqMY8`mzNL`Es?*Lswsi${VBtC8pryl+85Y@f!zo;2(+z0RNxE_ z2b~38h}??bL}jIEFF=lLMCIwzF6$hO;Ndp#*eOf|(;#k33#c3c=RSd`f(u2un7FZPo zhS2_z&%1@=2PKb~A>RaiFMg4bTqyx^ksvCH`{a8OLXzEJQvMk%MYfKpGyTTx$m(^3e` zR~4|h?my~jBmKt;P~!}@x?6$^#F+y)2(EGlfhuRXVTO&`vw4D}Pl5g-pntzSG#%VX z8bTxSYk=@OAnHCNP83K`N3b3yZ8RHK<2;OJhb@+dI2%DVDNqm%oGyXtY1q-R3%4x? z8B?l^lsPzs54wRUQi(I0F(q~61KrJ7%Jt58wf(3 zGT>xjPXo&b0F4X4kP<{A2i`^xLs%bwI{@!j;z-jtZu8roe`E=~Nv(Z2kE$E{CXMszKIP>9xZqTDH{G=}E0+xSO zgCSlB)OEw{BUgO1F=PMlnj@|av*J=&KsRW6=lGu3gV4ur{H$Aqp=<*j^ryxfZdtdv zHVeMvN$ev-~IapoARt-|03u z9K4VOjNAl9?oVAVmtgT{vBPq!7uXkQ|Ig?LW#y0%I9kCCz{$^+WL3bp|0Yzj>$q+L z(}@PE`h;ICJ3j$=!e9>u_n@{9$QN*0#71q?NTh1`|Lw+KH}{4euE)m%FI&C=tm9zk zw||6cdG0yP{&!8SI`&@a|4#+}K6_STcO|&Jjdwq=GTa|0s<9tG&M3k^@sNQq$Ab>p zALje__N4rihDE*XAAJH=s)3dN#J&zhgu?;F9hSfU40IVs;{<@!M+h5tl`nw$4uhj& zliN}n7-Ehs_;is5<3sU#aUp}?r4k#*T+I38|9_491@?H|NKsr3%Dpae9RQ*jA)K=y z^Uz_-+!X^N2V5dNg+-(ntG`{J7N9LM^i0e_xPGYqs+Rp1N) z+r7hZl@HvG;tmW+Rl<-AT;2m0JW|nnp`)PBGXCS%$tqa3Dc0D@ekna0uvB$|-PL~Q zGwSwq9r?TBzZn6#JeqM32qCs82ri9dLo7B5@W>Vz$Mz02q(F?HV6vq``E|g_|8AC^ zyu~^DeH6d!x6#PVMS(8S0oio_iFfZioN6$TXrc3C_%Y--2DG+x!BPaF=VM4wtSzH} z3bxV3qeynRAOtxy%z~oCebfVHfX@!tzx^jho_p-{|2c>t>1evh0b=zW6FLvb%WVi4rZs# zzDB;hdxq0N3=8-f@jb42>?UxO0`!jcKK>y=m|q(%x)@r%;~xzb!PI8m zJt{TUIBb+h8Vr)cz)&-${og1T8N&RRv4f>*0SKrwg$dYhq=SSP@f&jI1B;=bfdI*a z|9g~7X^v9}JirYTtf+=XAnMY2oG8$Pf>^5l5}-O+!&GeeXrP*LymV_kFl1)!CB98~cOc?3%xgsGhEIO?C0Jh+EkZ_0|8c|pcc z2kB-1&eFJRKmP%!hYf$y^kxIdTj+zPH}IeBX*U6ZYZwyS#1B014F(nj;RxipiR8qZ zkN`M{v_^;^p>IS2(C8-K-bJ{f&p1T+8!7#t9t58)|M?@Cg3^8?5C7*0=t93*D>KM5 zec)u8=O2Cog5%g0xbGEwVYQFz1DWVHCjo)jKCZG^*lc|yFeSl6ieWQRKPdiv0rSr+yT^k zS0S%I1o+gSsQD@}6yQP3D*Ep;0q_-htAADPe-v@R0oz3SKTY7T&iwi2jN(4)Zs40h zm|Y|tDfljp75u#!sBjlaPgLcO69;YXA{7ufJfIN-itT|hN(fPly*~0F6#gd{f67jYYL$Q;@haeOgN{Hwx?v4pE zBtyv%#l68}@A`=8p+dkwm<2790-G1fP)bBjeOQ?zawbPnBi<51u?!%b ziR7Sf=@IbF5oi{^xq`pBK#d}UG$>H=hyf%H-l9TLLutiGCTxshSv36nMwtx-j&YZf z1O-u00%4VZDeRCQ6)+)$5;$f?{SV_iN)WH_H2+W(*+~w=+(L+8x_`(iR465c8~9Qb zY%4HE4YA6Ey8*T;A{^M@8-A#f8Z@lO-}qL7XCDp1ga_cz&_K!ufc85aI7Yt%pcMu@ z8Dm1pLLW-;eo@c>mSe&=79$#zDWX6OchhkYg@ih3P!fnnNgQSGu@W{SdbFVBU1>bC z1_=Y?3Z6J&p;8z!QN=+i`rvzNU;aQx4g$#jC=LSpa1WTUt-@i5Ll-abAW8wDd;)jV z2u@icWjd5H!paaw8Kp;&LOT|O6wn#i(N}b!sqjf0c9H>g0BW}c=j-rk7FM+6A%I7l z;_v}$@N`V95`4gl0Z)k#{iktc5IcS>GvY8{?y%gWl0im?!3-I*!BMcP!IL2g#0C3* zD6I5g>^M5%ThRksUtZsXbr54NxEo-c1_E&dzOh5}-^LNy8ByAVKcN@~lmHaY2*!s} z&>lB6w95!|S=@&^?h0Zy0*8Q*Xd)=0;hQ56A2Z;v!QZH4f_THl>~Rw@pBZ@56pv#B z7s}Lr~P{b4(Lod2`z4p z{$K|-yv&1zD#OeikVYk#POcn4CsY1EWO)CLwVI9-v`{MDgV?cqODvQELvGc35C?RK z3(T8qwR;pRglzqu&Fs+eBfyJ@MjU1SJqbH>of-_MuSb9!!4{Z|MyRyyH8_cgeFKX! zflhG)XFYjP%!H4j05}9iIV6k_yi3FyvJDUfN&j9$7RZ$sbkXt%jxx-PI*B+w4vF)j zE+MYZLl{1kDpBD)3DmPm!UfI1d7U4O%uzaslOJV-NM(dv`B5i`2t<&O1RshKQd0yc z%!B+OM65CpsQ}7=XhIGNk!d4Yanm7C0L4rcqKJgjek zvlc`t6J60n;x-qlC&9N$Dg{B-E!s!g6-3Ds5$pfUwFt9X2%)5i3=IB5e-6_>8R6*% zEWy47-kV@U!y*i9Q#Qq7+|YSppbTM-=S&nvDH7eV#osAk0?F)$Fc4LI83}c5BRL>l z5nz0d3wZSG3!c_v`>I9+gqqzK>Jvff5DnZxLWJSqj9p6<$QAX&-(f^il0-ky_&b$I bBsKl16C_~wNk9-r7)F?#0(Qs#@a_KrlgPx1 delta 61992 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 diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java index 762c523b..c8181162 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java @@ -10,7 +10,6 @@ import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import java.util.ArrayList; import java.util.List; @@ -83,15 +82,13 @@ public static void main(String[] args) { // Each value reports one entry per token group, so a value can partially succeed. // requestId identifies the API call an error came from and is set on failures only. for (BulkTokenizeResponseRecord record : response.getRecords()) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() == null) { - System.out.printf("[%d] group '%s' -> %s%n", - record.getIndex(), token.getTokenGroupName(), token.getToken()); - } else { - System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", - record.getIndex(), token.getTokenGroupName(), - token.getHttpCode(), token.getError(), token.getRequestId()); - } + if (record.getError() == null) { + System.out.printf("[%d] group '%s' -> %s%n", + record.getIndex(), record.getTokenGroupName(), record.getToken()); + } else { + System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getTokenGroupName(), + record.getHttpCode(), record.getError(), record.getRequestId()); } } diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java index 9690f423..9d9c1248 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java @@ -10,7 +10,6 @@ import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import java.util.ArrayList; import java.util.List; @@ -87,15 +86,13 @@ public static void main(String[] args) { // partially succeed: some groups return a token while others return an error. requestId // identifies the API call an error came from and is set on failures only. for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() == null) { - System.out.printf("[%d] group '%s' -> %s%n", - record.getIndex(), token.getTokenGroupName(), token.getToken()); - } else { - System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", - record.getIndex(), token.getTokenGroupName(), - token.getHttpCode(), token.getError(), token.getRequestId()); - } + if (record.getError() == null) { + System.out.printf("[%d] group '%s' -> %s%n", + record.getIndex(), record.getTokenGroupName(), record.getToken()); + } else { + System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getTokenGroupName(), + record.getHttpCode(), record.getError(), record.getRequestId()); } } diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index d668f246..cfd8195f 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -35,7 +35,6 @@ import com.skyflow.vault.data.BulkTokenizeResponseRecord; import com.skyflow.vault.data.DeleteTokensRecord; import com.skyflow.vault.data.TokenizeRequestRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; @@ -677,17 +676,17 @@ public static List handleBulkTokenizeBatchException( if (batchRecords == null) return errorRecords; for (int position = 0; position < batchRecords.size(); position++) { BulkTokenizeRequestRecord requested = batchRecords.get(position); - List tokens = new ArrayList<>(); + int index = startIndex + position; List groupNames = requested.getTokenGroupNames(); if (groupNames == null || groupNames.isEmpty()) { - tokens.add(new TokenizeResponseToken(null, null, httpCode, message, requestId)); + errorRecords.add(new BulkTokenizeResponseRecord( + index, requested.getValue(), null, null, httpCode, message, requestId)); } else { for (String groupName : groupNames) { - tokens.add(new TokenizeResponseToken(groupName, null, httpCode, message, requestId)); + errorRecords.add(new BulkTokenizeResponseRecord( + index, requested.getValue(), groupName, null, httpCode, message, requestId)); } } - errorRecords.add(new BulkTokenizeResponseRecord( - startIndex + position, requested.getValue(), tokens)); } return errorRecords; } @@ -849,20 +848,20 @@ private static V1FlowTokenizeRequestObject buildTokenizeRequestObject(TokenizeRe return builder.build(); } - private static List buildTokenizeResponseTokens( - V1FlowTokenizeResponseObject record, String requestId) { + private static BulkTokenizeResponseRecord buildTokenizeResponseRecord( + int index, V1FlowTokenizeResponseObject record, String requestId) { boolean failed = record.getError().isPresent() && record.getError().get() != null && !record.getError().get().isEmpty(); - List tokens = new ArrayList<>(); - tokens.add(new TokenizeResponseToken( + return new BulkTokenizeResponseRecord( + index, + record.getValue().orElse(null), asNonEmptyString(record.getTokenGroupName().orElse(null)), asNonEmptyString(record.getToken().orElse(null)), record.getHttpCode().orElse(failed ? 500 : 200), failed ? record.getError().get() : null, requestId - )); - return tokens; + ); } /** The API sends "" for a token or error that does not apply; normalise both to null. */ @@ -900,18 +899,15 @@ public static BulkTokenizeResponse formatBulkTokenizeResponse( } /** - * Folds the response rows back onto the records that produced them. + * Assigns each response row the index of the record that produced it. * *

The API emits one row per (value, token group) rather than one per record, and a record * rejected outright yields a single row instead of one per group — so row count is not a * function of the request. Rows do arrive in request order, though, and each carries its value, - * which is enough: a row belongs to the record being filled while it matches that record's value + * which is enough: a row belongs to the record in flight while it matches that record's value * and the record has not yet taken as many rows as it asked for token groups. Anything else * starts the next record. Batching keeps values distinct within a request (see * {@link #createBulkTokenizeBatches}), so the value comparison never straddles two records. - * - *

A response already grouped one-row-per-record folds through this unchanged, since each row - * then matches exactly one record before the value moves on. */ private static List groupTokenizeRows( List rows, @@ -922,43 +918,29 @@ private static List groupTokenizeRows( if (batchRecords == null || batchRecords.isEmpty()) { // nothing to correlate against; fall back to one record per row for (int position = 0; position < rows.size(); position++) { - responseRecords.add(new BulkTokenizeResponseRecord(startIndex + position, - rows.get(position).getValue().orElse(null), - buildTokenizeResponseTokens(rows.get(position), requestId))); + responseRecords.add(buildTokenizeResponseRecord(startIndex + position, rows.get(position), requestId)); } return responseRecords; } int recordPosition = 0; int rowsTakenByRecord = 0; - List tokens = new ArrayList<>(); for (V1FlowTokenizeResponseObject row : rows) { Object rowValue = row.getValue().orElse(null); while (recordPosition < batchRecords.size() && !acceptsRow(batchRecords.get(recordPosition), rowValue, rowsTakenByRecord)) { - responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, - batchRecords.get(recordPosition).getValue(), tokens)); - tokens = new ArrayList<>(); rowsTakenByRecord = 0; recordPosition++; } if (recordPosition >= batchRecords.size()) { // more rows than the request can account for; keep them rather than drop them - responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, - rowValue, buildTokenizeResponseTokens(row, requestId))); + responseRecords.add(buildTokenizeResponseRecord(startIndex + recordPosition, row, requestId)); recordPosition++; continue; } - tokens.addAll(buildTokenizeResponseTokens(row, requestId)); + responseRecords.add(buildTokenizeResponseRecord(startIndex + recordPosition, row, requestId)); rowsTakenByRecord++; } - // close the record in flight, then any records the response never mentioned - while (recordPosition < batchRecords.size()) { - responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, - batchRecords.get(recordPosition).getValue(), tokens)); - tokens = new ArrayList<>(); - recordPosition++; - } return responseRecords; } diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java index 0d6d4618..37c2182e 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -39,7 +39,6 @@ import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java index 115fb9b3..26cf0281 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java @@ -5,7 +5,11 @@ import com.google.gson.annotations.Expose; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; public class BulkTokenizeResponse { @Expose(serialize = true) @@ -34,35 +38,45 @@ public BulkTokenizeResponse(List records, */ private static TokenizeSummary buildSummary(List records, List originalPayload) { + Map outcomesByIndex = new LinkedHashMap<>(); // index -> [succeeded, failed] + if (records != null) { + for (BulkTokenizeResponseRecord record : records) { + int[] outcomes = outcomesByIndex.computeIfAbsent(record.getIndex(), key -> new int[2]); + if (record.getError() == null) { + outcomes[0]++; + } else { + outcomes[1]++; + } + } + } int totalTokenized = 0; int totalPartial = 0; int totalFailed = 0; - if (records != null) { - for (BulkTokenizeResponseRecord record : records) { - int succeeded = 0; - int failed = 0; - if (record.getTokens() != null) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() == null) { - succeeded++; - } else { - failed++; - } - } + int totalTokens; + if (originalPayload != null) { + totalTokens = originalPayload.size(); + for (int index = 0; index < originalPayload.size(); index++) { + int[] outcomes = outcomesByIndex.getOrDefault(index, new int[2]); + if (outcomes[0] > 0 && outcomes[1] > 0) { + totalPartial++; + } else if (outcomes[0] > 0) { + totalTokenized++; + } else { + totalFailed++; } - if (succeeded > 0 && failed > 0) { + } + } else { + totalTokens = records != null ? records.size() : 0; + for (int[] outcomes : outcomesByIndex.values()) { + if (outcomes[0] > 0 && outcomes[1] > 0) { totalPartial++; - } else if (succeeded > 0) { + } else if (outcomes[0] > 0) { totalTokenized++; } else { - // no token groups came back, or every one of them failed totalFailed++; } } } - int totalTokens = originalPayload != null - ? originalPayload.size() - : (records != null ? records.size() : 0); return new TokenizeSummary(totalTokens, totalTokenized, totalPartial, totalFailed); } @@ -79,41 +93,33 @@ public List getRecords() { * *

Retryable means a 5xx other than 529, matching the rule used elsewhere in the SDK. The * caller's original record objects are returned unchanged — they carry no index, exactly as they - * were supplied. Records where nothing failed retryably are omitted. + * were supplied. */ public List getRecordsToRetry() { if (recordsToRetry == null) { recordsToRetry = new ArrayList<>(); if (records != null && originalPayload != null) { + Set retryableIndexes = new LinkedHashSet<>(); for (BulkTokenizeResponseRecord record : records) { + if (isRetryable(record)) { + retryableIndexes.add(record.getIndex()); + } + } + for (int index : retryableIndexes) { // the SDK assigns the index from the record's position in originalPayload, so a // positional lookup is exact - int index = record.getIndex(); - if (index < 0 || index >= originalPayload.size() || !hasRetryableFailure(record)) { - continue; + if (index >= 0 && index < originalPayload.size()) { + recordsToRetry.add(originalPayload.get(index)); } - recordsToRetry.add(originalPayload.get(index)); } } } return recordsToRetry; } - private static boolean hasRetryableFailure(BulkTokenizeResponseRecord record) { - if (record.getTokens() == null) { - return false; - } - for (TokenizeResponseToken token : record.getTokens()) { - if (isRetryable(token)) { - return true; - } - } - return false; - } - - private static boolean isRetryable(TokenizeResponseToken token) { - Integer httpCode = token.getHttpCode(); - return token.getError() != null + private static boolean isRetryable(BulkTokenizeResponseRecord record) { + Integer httpCode = record.getHttpCode(); + return record.getError() != null && httpCode != null && httpCode >= 500 && httpCode <= 599 && httpCode != 529; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java index bfcae073..d9baf9fb 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java @@ -4,18 +4,17 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; -import java.util.List; - /** * A {@link TokenizeResponseRecord} carrying the index of the input value it belongs to. The index - * is the one supplied on the matching {@link BulkTokenizeRequestRecord}, echoed back unchanged. + * is assigned by the SDK from the matching {@link BulkTokenizeRequestRecord}'s position. */ public class BulkTokenizeResponseRecord extends TokenizeResponseRecord { @Expose(serialize = true) private final int index; - public BulkTokenizeResponseRecord(int index, Object value, List tokens) { - super(value, tokens); + public BulkTokenizeResponseRecord(int index, Object value, String tokenGroupName, String token, + Integer httpCode, String error, String requestId) { + super(value, tokenGroupName, token, httpCode, error, requestId); this.index = index; } @@ -28,4 +27,4 @@ public String toString() { Gson gson = new GsonBuilder().serializeNulls().create(); return gson.toJson(this); } -} +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java index 84da4cef..9ca53eec 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java @@ -4,30 +4,70 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; -import java.util.List; - /** - * The tokenization outcome for one input value: every requested token group is reported in - * {@code tokens}, whether it succeeded or failed. + * One (value, token-group) outcome. {@code token} is populated on success and {@code error} on + * failure; {@code httpCode} is present on both paths. + * + *

{@code requestId} identifies the API call this outcome came from and is set only when the + * outcome is an error, since that is when it is useful for support. */ public class TokenizeResponseRecord { @Expose(serialize = true) private final Object value; @Expose(serialize = true) - private final List tokens; + private final String tokenGroupName; + + @Expose(serialize = true) + private final String token; + + @Expose(serialize = true) + private final Integer httpCode; - public TokenizeResponseRecord(Object value, List tokens) { + @Expose(serialize = true) + private final String error; + + @Expose(serialize = true) + private final String requestId; + + public TokenizeResponseRecord(Object value, String tokenGroupName, String token, Integer httpCode, String error) { + this(value, tokenGroupName, token, httpCode, error, null); + } + + public TokenizeResponseRecord(Object value, String tokenGroupName, String token, Integer httpCode, + String error, String requestId) { this.value = value; - this.tokens = tokens; + this.tokenGroupName = tokenGroupName; + this.token = token; + this.httpCode = httpCode; + this.error = error; + // a successful outcome carries no request id, whatever the caller passed + this.requestId = error != null ? requestId : null; } public Object getValue() { return value; } - public List getTokens() { - return tokens; + public String getTokenGroupName() { + return tokenGroupName; + } + + public String getToken() { + return token; + } + + public Integer getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; } @Override @@ -35,4 +75,4 @@ public String toString() { Gson gson = new GsonBuilder().serializeNulls().create(); return gson.toJson(this); } -} +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java deleted file mode 100644 index dff54b77..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; - -/** - * One token-group outcome for a single input value. {@code token} is populated on success and - * {@code error} on failure; {@code httpCode} is present on both paths. - * - *

{@code requestId} identifies the API call this outcome came from and is set only when the - * outcome is an error, since that is when it is useful for support. Bulk requests are split into - * batches, so every error from the same batch carries the same id and errors from different - * batches carry different ones. - */ -public class TokenizeResponseToken { - @Expose(serialize = true) - private final String tokenGroupName; - - @Expose(serialize = true) - private final String token; - - @Expose(serialize = true) - private final Integer httpCode; - - @Expose(serialize = true) - private final String error; - - @Expose(serialize = true) - private final String requestId; - - public TokenizeResponseToken(String tokenGroupName, String token, Integer httpCode, String error) { - this(tokenGroupName, token, httpCode, error, null); - } - - public TokenizeResponseToken(String tokenGroupName, String token, Integer httpCode, - String error, String requestId) { - this.tokenGroupName = tokenGroupName; - this.token = token; - this.httpCode = httpCode; - this.error = error; - // a successful outcome carries no request id, whatever the caller passed - this.requestId = error != null ? requestId : null; - } - - public String getTokenGroupName() { - return tokenGroupName; - } - - public String getToken() { - return token; - } - - public Integer getHttpCode() { - return httpCode; - } - - public String getError() { - return error; - } - - /** The API call this outcome came from; null unless this is an error. */ - public String getRequestId() { - return requestId; - } - - @Override - public String toString() { - Gson gson = new GsonBuilder().serializeNulls().create(); - return gson.toJson(this); - } -} diff --git a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java index 5c2994ae..830dc905 100644 --- a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java @@ -6,7 +6,6 @@ import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import org.junit.Assert; import org.junit.Test; @@ -21,8 +20,8 @@ /** * The API returns one flat row per (value, token group) instead of the nested {@code tokens} array * the generated wire type models, and a record rejected outright yields a single row regardless of - * how many groups it asked for. These cover folding those rows back onto the records that produced - * them. + * how many groups it asked for. These cover assigning each row the index of the record that + * produced it. */ public class FlatTokenizeResponseTests { @@ -59,7 +58,7 @@ private static BulkTokenizeRequestRecord byotRecord(Object value, String token, + "}"; @Test - public void testLiveResponse_threeRowsFoldOntoTwoRecords() { + public void testLiveResponse_threeRowsStayFlatWithSharedIndexes() { Map objectValue = new LinkedHashMap<>(); objectValue.put("email", "ka@yahoo.com"); objectValue.put("age", 28); @@ -71,27 +70,26 @@ public void testLiveResponse_threeRowsFoldOntoTwoRecords() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(LIVE_RESPONSE), sent, 0, new HashMap<>()); - // two inputs in, two records out - not three - Assert.assertEquals(2, result.getRecords().size()); + // three rows in, three flat records out - the object's two rows share its index + Assert.assertEquals(3, result.getRecords().size()); BulkTokenizeResponseRecord byot = result.getRecords().get(0); Assert.assertEquals(0, byot.getIndex()); Assert.assertEquals("byot-input-value", byot.getValue()); - Assert.assertEquals(1, byot.getTokens().size()); - Assert.assertEquals("Invalid request. BYOT token should contain one token group.", - byot.getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(400), byot.getTokens().get(0).getHttpCode()); + Assert.assertEquals("Invalid request. BYOT token should contain one token group.", byot.getError()); + Assert.assertEquals(Integer.valueOf(400), byot.getHttpCode()); // the API sends "" for a token that does not apply - Assert.assertNull(byot.getTokens().get(0).getToken()); - - BulkTokenizeResponseRecord object = result.getRecords().get(1); - Assert.assertEquals(1, object.getIndex()); - Assert.assertEquals(2, object.getTokens().size()); - Assert.assertEquals("deterministic_string_tg", object.getTokens().get(0).getTokenGroupName()); - Assert.assertEquals("cc1179a3-e2be-404e-9a31-4f97f27bf406", object.getTokens().get(0).getToken()); - Assert.assertNull(object.getTokens().get(0).getError()); + Assert.assertNull(byot.getToken()); + + BulkTokenizeResponseRecord objectRow1 = result.getRecords().get(1); + BulkTokenizeResponseRecord objectRow2 = result.getRecords().get(2); + Assert.assertEquals(1, objectRow1.getIndex()); + Assert.assertEquals(1, objectRow2.getIndex()); + Assert.assertEquals("deterministic_string_tg", objectRow1.getTokenGroupName()); + Assert.assertEquals("cc1179a3-e2be-404e-9a31-4f97f27bf406", objectRow1.getToken()); + Assert.assertNull(objectRow1.getError()); Assert.assertEquals("Tokenize failed. Token group emailTokenGroup is invalid. Specify a valid token group.", - object.getTokens().get(1).getError()); + objectRow2.getError()); } @Test @@ -118,7 +116,7 @@ public void testLiveResponse_summaryClassifiesByRecordNotByRow() { // ── duplicate token groups within one record ─────────────────────────────── @Test - public void testDuplicateTokenGroupsInOneRecord_bothRowsKeptUnderOneRecord() { + public void testDuplicateTokenGroupsInOneRecord_bothRowsShareTheSameIndex() { String json = "{\"response\": [" + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-b\", \"httpCode\": 200}" @@ -127,13 +125,13 @@ public void testDuplicateTokenGroupsInOneRecord_bothRowsKeptUnderOneRecord() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); - // one record asked for the same group twice; both results stay on it rather than + // one record asked for the same group twice; both rows share its index rather than // collapsing or spilling into a phantom second record - Assert.assertEquals(1, result.getRecords().size()); - List tokens = result.getRecords().get(0).getTokens(); - Assert.assertEquals(2, tokens.size()); - Assert.assertEquals("tok-a", tokens.get(0).getToken()); - Assert.assertEquals("tok-b", tokens.get(1).getToken()); + Assert.assertEquals(2, result.getRecords().size()); + Assert.assertEquals(0, result.getRecords().get(0).getIndex()); + Assert.assertEquals(0, result.getRecords().get(1).getIndex()); + Assert.assertEquals("tok-a", result.getRecords().get(0).getToken()); + Assert.assertEquals("tok-b", result.getRecords().get(1).getToken()); } // ── boundaries ───────────────────────────────────────────────────────────── @@ -151,11 +149,12 @@ public void testRecordRejectedOutright_nextRecordStillGetsItsOwnRows() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); - Assert.assertEquals(2, result.getRecords().size()); - Assert.assertEquals(1, result.getRecords().get(0).getTokens().size()); - Assert.assertEquals("rejected", result.getRecords().get(0).getTokens().get(0).getError()); - Assert.assertEquals(2, result.getRecords().get(1).getTokens().size()); - Assert.assertEquals("tok-1", result.getRecords().get(1).getTokens().get(0).getToken()); + Assert.assertEquals(3, result.getRecords().size()); + Assert.assertEquals(0, result.getRecords().get(0).getIndex()); + Assert.assertEquals("rejected", result.getRecords().get(0).getError()); + Assert.assertEquals(1, result.getRecords().get(1).getIndex()); + Assert.assertEquals(1, result.getRecords().get(2).getIndex()); + Assert.assertEquals("tok-1", result.getRecords().get(1).getToken()); } @Test @@ -173,7 +172,7 @@ public void testIndexIsOffsetByBatchStart() { } @Test - public void testRecordTheResponseNeverMentions_stillReportedWithNoTokens() { + public void testRecordTheResponseNeverMentions_hasNoFlatRowButStillCountsAsFailed() { String json = "{\"response\": [" + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-0\", \"httpCode\": 200}" + "]}"; @@ -181,10 +180,12 @@ public void testRecordTheResponseNeverMentions_stillReportedWithNoTokens() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); - // the caller must still see a record per input, so indexes stay aligned with their list - Assert.assertEquals(2, result.getRecords().size()); - Assert.assertEquals(1, result.getRecords().get(1).getIndex()); - Assert.assertTrue(result.getRecords().get(1).getTokens().isEmpty()); + // v1 got no row at all, so there is nothing flat to report for it - but the summary, + // which classifies by input index rather than by row, still counts it as failed + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(0, result.getRecords().get(0).getIndex()); + Assert.assertEquals(1, + new BulkTokenizeResponse(result.getRecords(), sent).getSummary().getTotalFailed()); } @Test @@ -198,11 +199,11 @@ public void testMoreRowsThanTheRequestExplains_rowsAreKeptNotDropped() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); Assert.assertEquals(2, result.getRecords().size()); - Assert.assertEquals("tok-9", result.getRecords().get(1).getTokens().get(0).getToken()); + Assert.assertEquals("tok-9", result.getRecords().get(1).getToken()); } @Test - public void testMultipleGroupsForOneValue_foldOntoOneRecord() { + public void testMultipleGroupsForOneValue_shareTheSameIndex() { String json = "{\"response\": [" + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," + "{\"value\": \"v0\", \"tokenGroupName\": \"g2\", \"token\": \"tok-b\", \"httpCode\": 200}," @@ -213,9 +214,11 @@ public void testMultipleGroupsForOneValue_foldOntoOneRecord() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); - Assert.assertEquals(2, result.getRecords().size()); - Assert.assertEquals(2, result.getRecords().get(0).getTokens().size()); - Assert.assertEquals("tok-c", result.getRecords().get(1).getTokens().get(0).getToken()); + Assert.assertEquals(3, result.getRecords().size()); + Assert.assertEquals(0, result.getRecords().get(0).getIndex()); + Assert.assertEquals(0, result.getRecords().get(1).getIndex()); + Assert.assertEquals(1, result.getRecords().get(2).getIndex()); + Assert.assertEquals("tok-c", result.getRecords().get(2).getToken()); } // ── rejected requests still describe each record in the body ─────────────── @@ -252,13 +255,12 @@ public void testRejectedRequest_reportsTheApiMessageNotJustTheStatus() { List records = Utils.handleBulkTokenizeBatchException(ex, sent, 0); - Assert.assertEquals(1, records.size()); // one entry, matching the API - not one fabricated per requested token group - Assert.assertEquals(1, records.get(0).getTokens().size()); + Assert.assertEquals(1, records.size()); Assert.assertEquals("Invalid request. BYOT token should contain one token group.", - records.get(0).getTokens().get(0).getError()); - Assert.assertNull(records.get(0).getTokens().get(0).getTokenGroupName()); - Assert.assertEquals(Integer.valueOf(400), records.get(0).getTokens().get(0).getHttpCode()); + records.get(0).getError()); + Assert.assertNull(records.get(0).getTokenGroupName()); + Assert.assertEquals(Integer.valueOf(400), records.get(0).getHttpCode()); } @Test @@ -276,8 +278,8 @@ public void testRejectedRequest_keepsPerRecordMessagesAndIndexes() { Assert.assertEquals(2, records.size()); Assert.assertEquals(20, records.get(0).getIndex()); Assert.assertEquals(21, records.get(1).getIndex()); - Assert.assertEquals(message, records.get(0).getTokens().get(0).getError()); - Assert.assertEquals(message, records.get(1).getTokens().get(0).getError()); + Assert.assertEquals(message, records.get(0).getError()); + Assert.assertEquals(message, records.get(1).getError()); } @Test @@ -289,11 +291,12 @@ public void testRejectedRequest_withNoUsableBodyFallsBackToTheStatusCode() { List records = Utils.handleBulkTokenizeBatchException(ex, sent, 0); - Assert.assertEquals(1, records.size()); - // no rows to go on, so every requested group is reported as failed - Assert.assertEquals(2, records.get(0).getTokens().size()); - Assert.assertEquals("connection reset", records.get(0).getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(500), records.get(0).getTokens().get(0).getHttpCode()); + // no rows to go on, so every requested group is reported as its own failed record + Assert.assertEquals(2, records.size()); + Assert.assertEquals(0, records.get(0).getIndex()); + Assert.assertEquals(0, records.get(1).getIndex()); + Assert.assertEquals("connection reset", records.get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), records.get(0).getHttpCode()); } @Test @@ -306,8 +309,8 @@ public void testRejectedRequest_withUnfamiliarBodyFallsBackToTheStatusCode() { Utils.handleBulkTokenizeBatchException(rejected(504, opaque), sent, 0); Assert.assertEquals(1, records.size()); - Assert.assertEquals("gateway timeout", records.get(0).getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(504), records.get(0).getTokens().get(0).getHttpCode()); + Assert.assertEquals("gateway timeout", records.get(0).getError()); + Assert.assertEquals(Integer.valueOf(504), records.get(0).getHttpCode()); } @Test diff --git a/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java b/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java index 753487d8..8572ba0d 100644 --- a/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java @@ -12,7 +12,6 @@ import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseToken; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; @@ -96,8 +95,8 @@ public void testTokenize_requestIdOnErrorsOnly() { BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( tokenizeWire(json), sent, 0, headers(REQ_ID_A)); - TokenizeResponseToken success = result.getRecords().get(0).getTokens().get(0); - TokenizeResponseToken failure = result.getRecords().get(1).getTokens().get(0); + BulkTokenizeResponseRecord success = result.getRecords().get(0); + BulkTokenizeResponseRecord failure = result.getRecords().get(1); Assert.assertNull("a successful token must not carry a request id", success.getRequestId()); Assert.assertEquals(REQ_ID_A, failure.getRequestId()); } @@ -144,10 +143,10 @@ public void testTokenize_twoBatchesReportTwoDistinctRequestIds() { List merged = new ArrayList<>(first.getRecords()); merged.addAll(second.getRecords()); Assert.assertEquals(4, merged.size()); - Assert.assertEquals(REQ_ID_A, merged.get(0).getTokens().get(0).getRequestId()); - Assert.assertEquals(REQ_ID_A, merged.get(1).getTokens().get(0).getRequestId()); - Assert.assertEquals(REQ_ID_B, merged.get(2).getTokens().get(0).getRequestId()); - Assert.assertEquals(REQ_ID_B, merged.get(3).getTokens().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, merged.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, merged.get(1).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(2).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(3).getRequestId()); Assert.assertEquals(2, merged.get(2).getIndex()); Assert.assertEquals(3, merged.get(3).getIndex()); } @@ -160,7 +159,7 @@ public void testTokenize_missingHeaderLeavesRequestIdNull() { tokenizeWire(json), Collections.singletonList(tokenizeRecord("v0", "g1")), 0, headers(null)); - Assert.assertNull(result.getRecords().get(0).getTokens().get(0).getRequestId()); + Assert.assertNull(result.getRecords().get(0).getRequestId()); } @Test @@ -173,10 +172,10 @@ public void testTokenize_partialRecordStampsOnlyTheFailedGroup() { tokenizeWire(json), Collections.singletonList(tokenizeRecord("v0", "g1", "g2")), 0, headers(REQ_ID_A)); - List tokens = result.getRecords().get(0).getTokens(); - Assert.assertEquals(2, tokens.size()); - Assert.assertNull(tokens.get(0).getRequestId()); - Assert.assertEquals(REQ_ID_A, tokens.get(1).getRequestId()); + List records = result.getRecords(); + Assert.assertEquals(2, records.size()); + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, records.get(1).getRequestId()); } // ── tokenize: rejected requests ──────────────────────────────────────────── @@ -198,7 +197,7 @@ public void testTokenize_rejectedRequestStampsIdFromTheFailedCall() { List records = Utils.handleBulkTokenizeBatchException( ex, Collections.singletonList(tokenizeRecord("v0", "g1")), 0); - Assert.assertEquals(REQ_ID_B, records.get(0).getTokens().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_B, records.get(0).getRequestId()); } @Test @@ -209,10 +208,9 @@ public void testTokenize_rejectedRequestWithNoBodyStillStampsEveryGroup() { List records = Utils.handleBulkTokenizeBatchException( ex, Collections.singletonList(tokenizeRecord("v0", "g1", "g2")), 0); - List tokens = records.get(0).getTokens(); - Assert.assertEquals(2, tokens.size()); - Assert.assertEquals(REQ_ID_A, tokens.get(0).getRequestId()); - Assert.assertEquals(REQ_ID_A, tokens.get(1).getRequestId()); + Assert.assertEquals(2, records.size()); + Assert.assertEquals(REQ_ID_A, records.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, records.get(1).getRequestId()); } @Test @@ -229,7 +227,7 @@ public void testTokenize_transportFailureReportsTheInnermostCause() { List records = Utils.handleBulkTokenizeBatchException( ex, Collections.singletonList(tokenizeRecord("v0", "g1")), 0); - String error = records.get(0).getTokens().get(0).getError(); + String error = records.get(0).getError(); Assert.assertTrue("expected the DNS failure, got: " + error, error.contains("UnknownHostException")); Assert.assertTrue(error.contains("badcluster.skyvault.skyflowapis.dev")); @@ -261,8 +259,8 @@ public void testTokenize_transportFailureHasNoRequestId() { new RuntimeException("connection reset"), Collections.singletonList(tokenizeRecord("v0", "g1")), 0); - Assert.assertNull(records.get(0).getTokens().get(0).getRequestId()); - Assert.assertEquals("connection reset", records.get(0).getTokens().get(0).getError()); + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals("connection reset", records.get(0).getError()); } // ── delete: success carries no id, errors carry the batch's ──────────────── @@ -430,11 +428,9 @@ public void testRequestIdAppearsInToString() { private static Set tokenizeRequestIds(List records) { Set ids = new HashSet<>(); for (BulkTokenizeResponseRecord record : records) { - for (TokenizeResponseToken token : record.getTokens()) { - if (token.getError() != null) { - Assert.assertNotNull("every error must carry a request id", token.getRequestId()); - ids.add(token.getRequestId()); - } + if (record.getError() != null) { + Assert.assertNotNull("every error must carry a request id", record.getRequestId()); + ids.add(record.getRequestId()); } } return ids; diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 9a2bd46a..55f01a66 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -38,13 +38,11 @@ import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.TokenizeResponseRecord; import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.TokenizeRequest; import com.skyflow.vault.data.TokenizeResponse; import com.skyflow.vault.data.UpsertOptions; import org.junit.After; -import com.skyflow.vault.data.TokenizeResponseToken; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -1553,18 +1551,17 @@ public void testHandleBulkTokenizeBatchException_apiExceptionFailsEveryTokenGrou List errors = Utils.handleBulkTokenizeBatchException( wrapper, tokenizeBatch("v1", "group1", "group2"), 0); - Assert.assertEquals(1, errors.size()); - BulkTokenizeResponseRecord record = errors.get(0); // index is derived from the batch position; the value is echoed from the request - Assert.assertEquals(0, record.getIndex()); - Assert.assertEquals("v1", record.getValue()); - // one failed token entry per requested group - Assert.assertEquals(2, record.getTokens().size()); - Assert.assertEquals("group1", record.getTokens().get(0).getTokenGroupName()); - Assert.assertEquals("invalid value", record.getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(400), record.getTokens().get(0).getHttpCode()); - Assert.assertEquals("group2", record.getTokens().get(1).getTokenGroupName()); - Assert.assertEquals("invalid value", record.getTokens().get(1).getError()); + // one failed record per requested group + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(0, errors.get(0).getIndex()); + Assert.assertEquals(0, errors.get(1).getIndex()); + Assert.assertEquals("v1", errors.get(0).getValue()); + Assert.assertEquals("group1", errors.get(0).getTokenGroupName()); + Assert.assertEquals("invalid value", errors.get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), errors.get(0).getHttpCode()); + Assert.assertEquals("group2", errors.get(1).getTokenGroupName()); + Assert.assertEquals("invalid value", errors.get(1).getError()); } @Test @@ -1577,8 +1574,8 @@ public void testHandleBulkTokenizeBatchException_genericException() { Assert.assertEquals(1, errors.size()); Assert.assertEquals(3, errors.get(0).getIndex()); - Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode()); - Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getHttpCode()); + Assert.assertEquals("boom", errors.get(0).getError()); } @Test @@ -1606,9 +1603,9 @@ public void testHandleBulkTokenizeBatchException_noTokenGroupsStillReportsOneEnt List errors = Utils.handleBulkTokenizeBatchException(ex, batch, 0); - Assert.assertEquals(1, errors.get(0).getTokens().size()); - Assert.assertNull(errors.get(0).getTokens().get(0).getTokenGroupName()); - Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(1, errors.size()); + Assert.assertNull(errors.get(0).getTokenGroupName()); + Assert.assertEquals("boom", errors.get(0).getError()); } @Test @@ -1630,11 +1627,9 @@ public void testHandleBulkTokenizeBatchException_errorBodyWithResponseArrayRebui Assert.assertEquals(1, errors.size()); Assert.assertEquals("v1", errors.get(0).getValue()); - Assert.assertEquals(1, errors.get(0).getTokens().size()); - Assert.assertEquals("group1", errors.get(0).getTokens().get(0).getTokenGroupName()); - Assert.assertEquals("BYOT token should contain one token group", - errors.get(0).getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(400), errors.get(0).getTokens().get(0).getHttpCode()); + Assert.assertEquals("group1", errors.get(0).getTokenGroupName()); + Assert.assertEquals("BYOT token should contain one token group", errors.get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), errors.get(0).getHttpCode()); } @Test @@ -1651,8 +1646,8 @@ public void testHandleBulkTokenizeBatchException_errorFieldAsObjectUsesStructure wrapper, tokenizeBatch("v1", "group1"), 0); Assert.assertEquals(1, errors.size()); - Assert.assertEquals("vault not found", errors.get(0).getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(404), errors.get(0).getTokens().get(0).getHttpCode()); + Assert.assertEquals("vault not found", errors.get(0).getError()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getHttpCode()); } @Test @@ -1665,8 +1660,8 @@ public void testHandleBulkTokenizeBatchException_nonMapBodyUsesApiMessage() { wrapper, tokenizeBatch("v1", "group1"), 0); Assert.assertEquals(1, errors.size()); - Assert.assertEquals("tokenize failed", errors.get(0).getTokens().get(0).getError()); - Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode()); + Assert.assertEquals("tokenize failed", errors.get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getHttpCode()); } @Test @@ -1947,8 +1942,8 @@ public void testFormatBulkTokenizeResponse_success() { BulkTokenizeResponseRecord record = result.getRecords().get(0); Assert.assertEquals(0, record.getIndex()); Assert.assertEquals("value1", record.getValue()); - Assert.assertEquals("tok-abc", record.getTokens().get(0).getToken()); - Assert.assertNull(record.getTokens().get(0).getError()); + Assert.assertEquals("tok-abc", record.getToken()); + Assert.assertNull(record.getError()); } @Test @@ -1960,9 +1955,9 @@ public void testFormatBulkTokenizeResponse_tokenError() { response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); Assert.assertEquals(1, result.getRecords().size()); - TokenizeResponseToken token = result.getRecords().get(0).getTokens().get(0); - Assert.assertEquals(Integer.valueOf(400), token.getHttpCode()); - Assert.assertEquals("invalid value", token.getError()); + BulkTokenizeResponseRecord record = result.getRecords().get(0); + Assert.assertEquals(Integer.valueOf(400), record.getHttpCode()); + Assert.assertEquals("invalid value", record.getError()); } @Test diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 2f1cd558..68ccfa06 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -409,8 +409,8 @@ public void testBulkTokenize_success() throws Exception { Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Assert.assertEquals(1, response.getRecords().size()); Assert.assertEquals(0, response.getRecords().get(0).getIndex()); - Assert.assertEquals("tok-abc", response.getRecords().get(0).getTokens().get(0).getToken()); - Assert.assertNull(response.getRecords().get(0).getTokens().get(0).getError()); + Assert.assertEquals("tok-abc", response.getRecords().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getError()); } @Test @@ -447,7 +447,7 @@ public void testBulkTokenizeAsync_success() throws Exception { BulkTokenizeResponse response = controller.bulkTokenizeAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Assert.assertEquals(1, response.getRecords().size()); - Assert.assertNull(response.getRecords().get(0).getTokens().get(0).getError()); + Assert.assertNull(response.getRecords().get(0).getError()); } // ── additional bulk API-error coverage ─────────────────────────────────── @@ -543,9 +543,8 @@ public void testBulkTokenize_apiErrorCapturedInErrors() throws Exception { Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Assert.assertEquals(1, response.getRecords().size()); Assert.assertEquals(0, response.getRecords().get(0).getIndex()); - Assert.assertEquals(Integer.valueOf(400), - response.getRecords().get(0).getTokens().get(0).getHttpCode()); - Assert.assertNotNull(response.getRecords().get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); } @Test @@ -566,9 +565,8 @@ public void testBulkTokenizeAsync_apiErrorCapturedInErrors() throws Exception { Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); Assert.assertEquals(1, response.getRecords().size()); Assert.assertEquals(0, response.getRecords().get(0).getIndex()); - Assert.assertEquals(Integer.valueOf(400), - response.getRecords().get(0).getTokens().get(0).getHttpCode()); - Assert.assertNotNull(response.getRecords().get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); } @Test diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java index 031099b4..be181eae 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -358,16 +358,9 @@ public void testBulkDeleteTokensResponse_toStringMatchesContractShape() { // ── BulkTokenizeResponse ───────────────────────────────────────────────── - private static BulkTokenizeResponseRecord tokenizeRecord(int index, Object value, TokenizeResponseToken... tokens) { - return new BulkTokenizeResponseRecord(index, value, Arrays.asList(tokens)); - } - - private static TokenizeResponseToken okToken(String group, String token) { - return new TokenizeResponseToken(group, token, 200, null); - } - - private static TokenizeResponseToken failedToken(String group, String error) { - return new TokenizeResponseToken(group, null, 400, error); + private static BulkTokenizeResponseRecord row(int index, Object value, String group, String token, + Integer httpCode, String error) { + return new BulkTokenizeResponseRecord(index, value, group, token, httpCode, error, null); } private static List payloadOf(int size) { @@ -394,15 +387,16 @@ public void testBulkTokenizeResponse_twoArgConstructorClassifiesEachValue() { // index 1: some ok, some failed -> totalPartial // index 2: all groups failed -> totalFailed List records = Arrays.asList( - tokenizeRecord(0, "v0", okToken("g1", "tok-0")), - tokenizeRecord(1, "v1", okToken("g1", "tok-1"), failedToken("g2", "partial failure")), - tokenizeRecord(2, "v2", failedToken("g1", "full failure"))); + row(0, "v0", "g1", "tok-0", 200, null), + row(1, "v1", "g1", "tok-1", 200, null), + row(1, "v1", "g2", null, 400, "partial failure"), + row(2, "v2", "g1", null, 400, "full failure")); BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(3)); TokenizeSummary summary = response.getSummary(); Assert.assertNotNull(summary); - // totalTokens counts input values submitted, not output token entries + // totalTokens counts input values submitted, not output rows Assert.assertEquals(3, summary.getTotalTokens()); Assert.assertEquals(1, summary.getTotalTokenized()); Assert.assertEquals(1, summary.getTotalPartial()); @@ -410,11 +404,8 @@ public void testBulkTokenizeResponse_twoArgConstructorClassifiesEachValue() { } @Test - public void testBulkTokenizeResponse_recordWithNoTokensCountsAsFailed() { - List records = Collections.singletonList( - new BulkTokenizeResponseRecord(0, "v0", Collections.emptyList())); - - BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(1)); + public void testBulkTokenizeResponse_valueWithNoRowsCountsAsFailed() { + BulkTokenizeResponse response = new BulkTokenizeResponse(new ArrayList<>(), payloadOf(1)); Assert.assertEquals(0, response.getSummary().getTotalTokenized()); Assert.assertEquals(1, response.getSummary().getTotalFailed()); @@ -423,12 +414,11 @@ public void testBulkTokenizeResponse_recordWithNoTokensCountsAsFailed() { @Test public void testBulkTokenizeResponse_getRecordsToRetryReturnsCallerRecordUnchanged() { // one value, four groups: only the 503 is retryable, but the whole record comes back - List records = Collections.singletonList( - tokenizeRecord(0, "9999999999", - okToken("phone_group", "p1q2r3s4"), - new TokenizeResponseToken("phone_group_2", null, 503, "unavailable"), - new TokenizeResponseToken("phone_group_3", null, 400, "bad group"), - new TokenizeResponseToken("phone_group_4", null, 529, "special case"))); + List records = Arrays.asList( + row(0, "9999999999", "phone_group", "p1q2r3s4", 200, null), + row(0, "9999999999", "phone_group_2", null, 503, "unavailable"), + row(0, "9999999999", "phone_group_3", null, 400, "bad group"), + row(0, "9999999999", "phone_group_4", null, 529, "special case")); BulkTokenizeRequestRecord requested = BulkTokenizeRequestRecord.builder().value("9999999999") .tokenGroupNames(Arrays.asList( "phone_group", "phone_group_2", "phone_group_3", "phone_group_4")) @@ -449,9 +439,9 @@ public void testBulkTokenizeResponse_getRecordsToRetryReturnsCallerRecordUnchang @Test public void testBulkTokenizeResponse_getRecordsToRetryOmitsValuesWithoutRetryableFailures() { List records = Arrays.asList( - tokenizeRecord(0, "v0", okToken("g1", "tok-0")), - tokenizeRecord(1, "v1", failedToken("g1", "bad group")), // 400, not retryable - tokenizeRecord(2, "v2", new TokenizeResponseToken("g1", null, 500, "server error"))); + row(0, "v0", "g1", "tok-0", 200, null), + row(1, "v1", "g1", null, 400, "bad group"), // not retryable + row(2, "v2", "g1", null, 500, "server error")); List retry = new BulkTokenizeResponse(records, payloadOf(3)).getRecordsToRetry(); @@ -464,8 +454,8 @@ public void testBulkTokenizeResponse_getRecordsToRetryOmitsValuesWithoutRetryabl public void testBulkTokenizeResponse_getRecordsToRetryLooksUpByIndexNotResponseOrder() { // batches finish out of order, so the failing record is not first in the response List records = Arrays.asList( - tokenizeRecord(2, "v2", new TokenizeResponseToken("g1", null, 500, "server error")), - tokenizeRecord(0, "v0", okToken("g1", "tok-0"))); + row(2, "v2", "g1", null, 500, "server error"), + row(0, "v0", "g1", "tok-0", 200, null)); List retry = new BulkTokenizeResponse(records, payloadOf(3)).getRecordsToRetry(); @@ -478,9 +468,9 @@ public void testBulkTokenizeResponse_getRecordsToRetryLooksUpByIndexNotResponseO public void testBulkTokenizeResponse_getRecordsToRetrySkipsIndexOutsidePayload() { // a malformed index must not blow up with IndexOutOfBoundsException List records = Arrays.asList( - tokenizeRecord(9, "stray", new TokenizeResponseToken("g1", null, 500, "server error")), - tokenizeRecord(-1, "stray", new TokenizeResponseToken("g1", null, 500, "server error")), - tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + row(9, "stray", "g1", null, 500, "server error"), + row(-1, "stray", "g1", null, 500, "server error"), + row(0, "v0", "g1", null, 500, "server error")); List retry = new BulkTokenizeResponse(records, payloadOf(1)).getRecordsToRetry(); @@ -492,7 +482,7 @@ public void testBulkTokenizeResponse_getRecordsToRetrySkipsIndexOutsidePayload() @Test public void testBulkTokenizeResponse_getRecordsToRetryCarriesByotToken() { List records = Collections.singletonList( - tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + row(0, "v0", "g1", null, 500, "server error")); List payload = Collections.singletonList( BulkTokenizeRequestRecord.builder().value("v0").token("my-own-token") .tokenGroupNames(Collections.singletonList("g1")).build()); @@ -505,9 +495,9 @@ public void testBulkTokenizeResponse_getRecordsToRetryCarriesByotToken() { @Test public void testBulkTokenizeResponse_getRecordsToRetryKeepsRequestedGroupsOnBatchFailure() { - // a batch-level failure reports no group name on the token entry + // a batch-level failure reports no group name on the row List records = Collections.singletonList( - tokenizeRecord(0, "v0", new TokenizeResponseToken(null, null, 500, "server error"))); + row(0, "v0", null, null, 500, "server error")); List payload = Collections.singletonList( BulkTokenizeRequestRecord.builder().value("v0") .tokenGroupNames(Arrays.asList("g1", "g2")).build()); @@ -521,7 +511,7 @@ public void testBulkTokenizeResponse_getRecordsToRetryKeepsRequestedGroupsOnBatc @Test public void testBulkTokenizeResponse_recordsToRetryNotSerialized() { List records = Collections.singletonList( - tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + row(0, "v0", "g1", null, 500, "server error")); BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(1)); response.getRecordsToRetry(); // populate the lazily-derived field @@ -532,10 +522,9 @@ public void testBulkTokenizeResponse_recordsToRetryNotSerialized() { @Test public void testBulkTokenizeResponse_toStringMatchesContractShape() { List records = Arrays.asList( - tokenizeRecord(0, "john@example.com", okToken("email_group", "a1b2c3d4")), - tokenizeRecord(1, "9999999999", - okToken("phone_group", "p1q2r3s4"), - failedToken("phone_group_2", "Invalid token group configuration"))); + row(0, "john@example.com", "email_group", "a1b2c3d4", 200, null), + row(1, "9999999999", "phone_group", "p1q2r3s4", 200, null), + row(1, "9999999999", "phone_group_2", null, 400, "Invalid token group configuration")); String json = new BulkTokenizeResponse(records, payloadOf(2)).toString(); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java index d8a1437a..eb118a3a 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java @@ -17,12 +17,8 @@ */ public class BulkRetryAndSummaryTests { - private static TokenizeResponseToken token(String group, String value, Integer httpCode, String error) { - return new TokenizeResponseToken(group, value, httpCode, error); - } - - private static BulkTokenizeResponseRecord tokenizeRecord(int index, TokenizeResponseToken... tokens) { - return new BulkTokenizeResponseRecord(index, "value" + index, Arrays.asList(tokens)); + private static BulkTokenizeResponseRecord row(int index, String group, String token, Integer httpCode, String error) { + return new BulkTokenizeResponseRecord(index, "value" + index, group, token, httpCode, error, null); } private static BulkTokenizeRequestRecord requestRecord(String value) { @@ -33,8 +29,9 @@ private static BulkTokenizeRequestRecord requestRecord(String value) { @Test public void testTokenizeSummary_allGroupsSucceededCountsAsTokenized() { - List records = Collections.singletonList( - tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", "t2", 200, null))); + List records = Arrays.asList( + row(0, "g1", "t1", 200, null), + row(0, "g2", "t2", 200, null)); TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getSummary(); @@ -47,8 +44,9 @@ public void testTokenizeSummary_allGroupsSucceededCountsAsTokenized() { @Test public void testTokenizeSummary_someGroupsFailedCountsAsPartial() { - List records = Collections.singletonList( - tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", null, 400, "bad group"))); + List records = Arrays.asList( + row(0, "g1", "t1", 200, null), + row(0, "g2", null, 400, "bad group")); TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getSummary(); @@ -60,8 +58,9 @@ public void testTokenizeSummary_someGroupsFailedCountsAsPartial() { @Test public void testTokenizeSummary_everyGroupFailedCountsAsFailed() { - List records = Collections.singletonList( - tokenizeRecord(0, token("g1", null, 500, "boom"), token("g2", null, 500, "boom"))); + List records = Arrays.asList( + row(0, "g1", null, 500, "boom"), + row(0, "g2", null, 500, "boom")); TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getSummary(); @@ -72,31 +71,19 @@ public void testTokenizeSummary_everyGroupFailedCountsAsFailed() { } @Test - public void testTokenizeSummary_noTokensAtAllCountsAsFailed() { - // A record that came back with no token groups is a failure, not a success. - List records = Collections.singletonList( - new BulkTokenizeResponseRecord(0, "value0", null)); - - TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + public void testTokenizeSummary_noRowsAtAllCountsAsFailed() { + // A value with no rows in the response at all is a failure, not a success. + TokenizeSummary summary = new BulkTokenizeResponse(new ArrayList<>(), Collections.singletonList(requestRecord("a"))) .getSummary(); Assert.assertEquals(1, summary.getTotalFailed()); } - @Test - public void testTokenizeSummary_emptyTokenListCountsAsFailed() { - List records = Collections.singletonList( - new BulkTokenizeResponseRecord(0, "value0", new ArrayList<>())); - - Assert.assertEquals(1, new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) - .getSummary().getTotalFailed()); - } - @Test public void testTokenizeSummary_totalTokensComesFromTheSubmittedPayload() { // Two values submitted, only one came back — totalTokens must reflect what was sent. List records = Collections.singletonList( - tokenizeRecord(0, token("g1", "t1", 200, null))); + row(0, "g1", "t1", 200, null)); TokenizeSummary summary = new BulkTokenizeResponse( records, Arrays.asList(requestRecord("a"), requestRecord("b"))).getSummary(); @@ -126,9 +113,9 @@ public void testTokenizeSummary_isNullWhenNoPayloadWasSupplied() { @Test public void testTokenizeRetry_only5xxFailuresAreReturned() { List records = Arrays.asList( - tokenizeRecord(0, token("g", null, 500, "server")), - tokenizeRecord(1, token("g", null, 400, "client")), - tokenizeRecord(2, token("g", "t", 200, null))); + row(0, "g", null, 500, "server"), + row(1, "g", null, 400, "client"), + row(2, "g", "t", 200, null)); List payload = Arrays.asList(requestRecord("a"), requestRecord("b"), requestRecord("c")); @@ -141,7 +128,7 @@ public void testTokenizeRetry_only5xxFailuresAreReturned() { @Test public void testTokenizeRetry_529IsNotRetried() { List records = Collections.singletonList( - tokenizeRecord(0, token("g", null, 529, "site frozen"))); + row(0, "g", null, 529, "site frozen")); Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().isEmpty()); @@ -150,17 +137,17 @@ public void testTokenizeRetry_529IsNotRetried() { @Test public void testTokenizeRetry_599IsRetriedAnd600IsNot() { Assert.assertEquals(1, new BulkTokenizeResponse( - Collections.singletonList(tokenizeRecord(0, token("g", null, 599, "edge"))), + Collections.singletonList(row(0, "g", null, 599, "edge")), Collections.singletonList(requestRecord("a"))).getRecordsToRetry().size()); Assert.assertEquals(0, new BulkTokenizeResponse( - Collections.singletonList(tokenizeRecord(0, token("g", null, 600, "edge"))), + Collections.singletonList(row(0, "g", null, 600, "edge")), Collections.singletonList(requestRecord("a"))).getRecordsToRetry().size()); } @Test public void testTokenizeRetry_5xxWithoutAnErrorIsNotRetried() { List records = Collections.singletonList( - tokenizeRecord(0, token("g", "t", 500, null))); + row(0, "g", "t", 500, null)); Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().isEmpty()); @@ -169,7 +156,7 @@ public void testTokenizeRetry_5xxWithoutAnErrorIsNotRetried() { @Test public void testTokenizeRetry_nullHttpCodeIsNotRetried() { List records = Collections.singletonList( - tokenizeRecord(0, token("g", null, null, "no status"))); + row(0, "g", null, null, "no status")); Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().isEmpty()); @@ -178,27 +165,25 @@ public void testTokenizeRetry_nullHttpCodeIsNotRetried() { @Test public void testTokenizeRetry_partialFailureRetriesTheWholeRecord() { // One group failed retryably, another succeeded — the value still needs resubmitting. - List records = Collections.singletonList( - tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", null, 503, "down"))); + List records = Arrays.asList( + row(0, "g1", "t1", 200, null), + row(0, "g2", null, 503, "down")); Assert.assertEquals(1, new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().size()); } @Test - public void testTokenizeRetry_recordWithNullTokensIsNotRetried() { - List records = Collections.singletonList( - new BulkTokenizeResponseRecord(0, "value0", null)); - - Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + public void testTokenizeRetry_recordWithNoRowsIsNotRetried() { + Assert.assertTrue(new BulkTokenizeResponse(new ArrayList<>(), Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().isEmpty()); } @Test public void testTokenizeRetry_outOfRangeIndexIsSkippedRatherThanThrowing() { List records = Arrays.asList( - tokenizeRecord(5, token("g", null, 500, "server")), - tokenizeRecord(-1, token("g", null, 500, "server"))); + row(5, "g", null, 500, "server"), + row(-1, "g", null, 500, "server")); Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) .getRecordsToRetry().isEmpty()); @@ -207,7 +192,7 @@ public void testTokenizeRetry_outOfRangeIndexIsSkippedRatherThanThrowing() { @Test public void testTokenizeRetry_withoutOriginalPayloadReturnsEmpty() { List records = Collections.singletonList( - tokenizeRecord(0, token("g", null, 500, "server"))); + row(0, "g", null, 500, "server")); Assert.assertTrue(new BulkTokenizeResponse(records).getRecordsToRetry().isEmpty()); } @@ -215,7 +200,7 @@ public void testTokenizeRetry_withoutOriginalPayloadReturnsEmpty() { @Test public void testTokenizeRetry_isMemoisedAcrossCalls() { List records = Collections.singletonList( - tokenizeRecord(0, token("g", null, 500, "server"))); + row(0, "g", null, 500, "server")); BulkTokenizeResponse response = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java index f3509eac..1b74171d 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java @@ -109,22 +109,20 @@ public void testTokenizeRequest_defaultIsNull() { @Test public void testTokenizeResponse_gettersReturnConstructorValues() { List records = Collections.singletonList( - new TokenizeResponseRecord("value1", Collections.singletonList( - new TokenizeResponseToken("group1", "tok-abc", 200, null)))); + new TokenizeResponseRecord("value1", "group1", "tok-abc", 200, null)); TokenizeResponse response = new TokenizeResponse(records); Assert.assertEquals(records, response.getResponse()); Assert.assertEquals("value1", response.getResponse().get(0).getValue()); - Assert.assertEquals("tok-abc", response.getResponse().get(0).getTokens().get(0).getToken()); - Assert.assertNull(response.getResponse().get(0).getTokens().get(0).getError()); + Assert.assertEquals("tok-abc", response.getResponse().get(0).getToken()); + Assert.assertNull(response.getResponse().get(0).getError()); } @Test public void testTokenizeResponse_toStringSerializesNulls() { TokenizeResponse response = new TokenizeResponse(Collections.singletonList( - new TokenizeResponseRecord("value1", Collections.singletonList( - new TokenizeResponseToken("group1", "tok-abc", 200, null))))); + new TokenizeResponseRecord("value1", "group1", "tok-abc", 200, null))); Assert.assertTrue(response.toString().contains("\"error\":null")); } diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index b97ae191..b77d704f 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -11,7 +11,7 @@ /** * Tests for the response/success/summary building-block classes that carry real - * constructor logic or toString() serialization: {@link Token}, {@link TokenizeResponseToken}, + * constructor logic or toString() serialization: {@link Token}, * {@link TokenizeResponseRecord}, {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, * {@link DeleteTokensRecord}, {@link BulkDeleteTokensResponseRecord}, * {@link DeleteTokensSummary}, {@link DetokenizeSummary}, {@link DetokenizeMetadata}, @@ -390,46 +390,36 @@ public void testBulkSummary_toStringNotNull() { } - // ── TokenizeResponseToken ──────────────────────────────────────────────── + // ── TokenizeResponseRecord / BulkTokenizeResponseRecord ────────────────── @Test - public void testTokenizeResponseToken_successValues() { - TokenizeResponseToken token = new TokenizeResponseToken("group1", "tok-abc", 200, null); - Assert.assertEquals("group1", token.getTokenGroupName()); - Assert.assertEquals("tok-abc", token.getToken()); - Assert.assertEquals(Integer.valueOf(200), token.getHttpCode()); - Assert.assertNull(token.getError()); + public void testTokenizeResponseRecord_successValues() { + TokenizeResponseRecord record = new TokenizeResponseRecord("value1", "group1", "tok-abc", 200, null); + Assert.assertEquals("value1", record.getValue()); + Assert.assertEquals("group1", record.getTokenGroupName()); + Assert.assertEquals("tok-abc", record.getToken()); + Assert.assertEquals(Integer.valueOf(200), record.getHttpCode()); + Assert.assertNull(record.getError()); } @Test - public void testTokenizeResponseToken_errorValues() { - TokenizeResponseToken token = new TokenizeResponseToken("group2", null, 400, "bad group"); - Assert.assertNull(token.getToken()); - Assert.assertEquals("bad group", token.getError()); - Assert.assertEquals(Integer.valueOf(400), token.getHttpCode()); + public void testTokenizeResponseRecord_errorValues() { + TokenizeResponseRecord record = new TokenizeResponseRecord("value1", "group2", null, 400, "bad group"); + Assert.assertNull(record.getToken()); + Assert.assertEquals("bad group", record.getError()); + Assert.assertEquals(Integer.valueOf(400), record.getHttpCode()); } @Test - public void testTokenizeResponseToken_toStringSerializesNulls() { - Assert.assertTrue(new TokenizeResponseToken("group1", "tok-abc", 200, null) + public void testTokenizeResponseRecord_toStringSerializesNulls() { + Assert.assertTrue(new TokenizeResponseRecord("value1", "group1", "tok-abc", 200, null) .toString().contains("\"error\":null")); } - // ── TokenizeResponseRecord / BulkTokenizeResponseRecord ────────────────── - - @Test - public void testTokenizeResponseRecord_gettersReturnConstructorValues() { - List tokens = Collections.singletonList( - new TokenizeResponseToken("group1", "tok-abc", 200, null)); - TokenizeResponseRecord record = new TokenizeResponseRecord("value1", tokens); - Assert.assertEquals("value1", record.getValue()); - Assert.assertEquals(tokens, record.getTokens()); - } - @Test public void testBulkTokenizeResponseRecord_carriesIndexAndIsATokenizeResponseRecord() { - BulkTokenizeResponseRecord record = new BulkTokenizeResponseRecord(7, "value1", - Collections.singletonList(new TokenizeResponseToken("group1", "tok-abc", 200, null))); + BulkTokenizeResponseRecord record = new BulkTokenizeResponseRecord( + 7, "value1", "group1", "tok-abc", 200, null, null); Assert.assertEquals(7, record.getIndex()); Assert.assertEquals("value1", record.getValue()); Assert.assertTrue(record instanceof TokenizeResponseRecord); From e0eb9af35b90c461feedc661a7c6a2a266a72e67 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 24 Aug 2026 15:58:06 +0530 Subject: [PATCH 4/6] chore: whitelist noextension in cspell word list DetectControllerTests.java's testGetBaseFileName_withoutExtensionReturnsWholeName (already merged on flowvault-release/26.8.13, ahead of this branch) uses the synthetic filename "noextension" as a test fixture, same pattern as the existing nocreds/nodir entries. Not otherwise related to this PR's Tokenize change; adding it here since it's what's blocking this PR's cspell check. --- .cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell.json b/.cspell.json index cd9d7cf3..4c434d07 100644 --- a/.cspell.json +++ b/.cspell.json @@ -97,6 +97,7 @@ "unrecognised", "nocreds", "nodir", + "noextension", "detok", "ngrok", "obac", From e7c54b2700e817a02d24d8a2f8a6401762d59515 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 24 Aug 2026 16:06:17 +0530 Subject: [PATCH 5/6] SK-3061: add BYOT-with-single-invalid-group coverage Closes the gap flagged earlier: every existing BYOT test only exercised naming too many groups (the 'should contain one token group' rejection). This covers a BYOT record naming exactly one group where that group itself is invalid - same error shape as the non-BYOT case, just on a BYOT record. Fixture verified against a live call (dev vault, single BYOT record naming one nonexistent group): the real API returns tokenGroupName=null, token=null, httpCode=400, and 'Tokenize failed. Token group X is invalid. Specify a valid token group.' - matching this test exactly. --- .../utils/FlatTokenizeResponseTests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java index 830dc905..c809063d 100644 --- a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java @@ -113,6 +113,31 @@ public void testLiveResponse_summaryClassifiesByRecordNotByRow() { Assert.assertEquals(1, withPayload.getSummary().getTotalFailed()); } + // ── BYOT naming exactly one, invalid, group ───────────────────────────────── + + @Test + public void testByotWithSingleInvalidGroup_reportsTheGroupErrorNotTheByotError() { + // BYOT naming exactly one group satisfies the "one token group" rule, so a bad group name + // fails for the same reason it would on a non-BYOT record, not for naming too many groups. + String json = "{\"response\": [" + + "{\"value\": \"byot-value\", \"tokenGroupName\": null, \"token\": \"\"," + + " \"error\": \"Tokenize failed. Token group bad_group is invalid. Specify a valid token group.\"," + + " \"httpCode\": 400}" + + "]}"; + List sent = Collections.singletonList( + byotRecord("byot-value", "my-own-token", "bad_group")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + BulkTokenizeResponseRecord record = result.getRecords().get(0); + Assert.assertEquals(0, record.getIndex()); + Assert.assertEquals("Tokenize failed. Token group bad_group is invalid. Specify a valid token group.", + record.getError()); + Assert.assertEquals(Integer.valueOf(400), record.getHttpCode()); + Assert.assertNull(record.getToken()); + } + // ── duplicate token groups within one record ─────────────────────────────── @Test From d0a1622808ef1e721b4358c7e64baef0393eeb28 Mon Sep 17 00:00:00 2001 From: Devesh Bhardwaj Date: Mon, 24 Aug 2026 16:15:06 +0530 Subject: [PATCH 6/6] SK-3061: cover partial tokenize failure with a retryable group through VaultController Closes the gap noted while discussing the retry path live: the only existing coverage of 'one value, one group succeeds, one group fails retryably (5xx)' was BulkResponseTests/BulkRetryAndSummaryTests exercising BulkTokenizeResponse directly. This runs the same scenario through VaultController.bulkTokenize() with a mocked raw client, so batching/index-assignment and getRecordsToRetry() are exercised together end to end, not just the summary math in isolation. A real 5xx can't be forced from the live API on demand (server-side failure, not something a crafted request triggers), so this is mocked, same as the existing apiErrorCapturedInErrors tests in this file. --- .../controller/VaultControllerTests.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 68ccfa06..8ddc2e1b 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -56,6 +56,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -413,6 +414,40 @@ public void testBulkTokenize_success() throws Exception { Assert.assertNull(response.getRecords().get(0).getError()); } + @Test + public void testBulkTokenize_partialFailureWithRetryableGroupIsRetriedAsWhole() throws Exception { + // one value, two groups: group1 succeeds, group2 fails with a retryable 503 + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1FlowTokenizeResponseObject succeeded = V1FlowTokenizeResponseObject.builder() + .value("value1").tokenGroupName("group1").token("tok-abc").httpCode(200).build(); + V1FlowTokenizeResponseObject failed = V1FlowTokenizeResponseObject.builder() + .value("value1").error("service unavailable").httpCode(503).build(); + V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() + .response(Arrays.asList(succeeded, failed)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.tokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + BulkTokenizeRequestRecord requested = BulkTokenizeRequestRecord.builder().value("value1") + .tokenGroupNames(Arrays.asList("group1", "group2")).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder() + .records(Collections.singletonList(requested)).build(); + + BulkTokenizeResponse response = controller.bulkTokenize(request); + + Assert.assertEquals(2, response.getRecords().size()); + Assert.assertEquals(0, response.getRecords().get(0).getIndex()); + Assert.assertEquals(0, response.getRecords().get(1).getIndex()); + Assert.assertEquals(1, response.getSummary().getTotalPartial()); + + List retry = response.getRecordsToRetry(); + Assert.assertEquals(1, retry.size()); + Assert.assertSame("must return the caller's own record, groups and all", requested, retry.get(0)); + } + @Test public void testBulkTokenize_nullRequestThrowsSkyflowException() throws Exception { ApiClient mockApi = Mockito.mock(ApiClient.class);