Skip to content

Commit 05ce8d0

Browse files
SK-3061: make DetokenizeResponseRecord.getMetadata() typed (#410)
* SK-3061: make DetokenizeResponseRecord.getMetadata() typed metadata was a raw Map<String, Object>, requiring callers to cast into it to reach skyflowId/tableName - the same usability gap Token closed for InsertResponseRecord.getTokens(). Added DetokenizeMetadata (getSkyflowId(), getTableName()) and made getMetadata() itself return it, matching how getTokens() was made typed directly rather than adding a second accessor. DetokenizeMetadata.parseMetadata(Map<String, Object>) normalizes the wire shape - the proto's own literal example uses {"table": ..., "skyflowID": ...}, but Utils.java's existing handling already renamed skyflowID -> skyflowId before this reached the record constructor, so parseMetadata accepts either casing for both keys and moves that normalization out of Utils.java and into one place, alongside the typing. This changes DetokenizeResponseRecord/BulkDetokenizeResponseRecord's metadata constructor parameter and getMetadata()'s return type from Map<String, Object> to DetokenizeMetadata - a japicmp-breaking change, same shape as the earlier getTokens() typing. Regenerated flowvault/api-report/skyflow-flowvault-java.baseline.jar via scripts/contract-snapshot-update.sh flowvault. Updated README's Bulk Detokenize section (JSON sample + a typed-access snippet) and every test constructing a record with a raw metadata map. mvn -pl common,flowvault -am test -> 702 tests, 0 failures. mvn -pl common,flowvault -am verify -> BUILD SUCCESS against the regenerated baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * SK-3061: add flowdb/unrenamed to cspell dictionary Flagged by the cspell CI check on PR #410 - both words come from the DetokenizeMetadata Javadoc/test comments referencing flowdb_dp_apis.proto and describing the wire's unrenamed table key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * SK-3061: add matching response-iteration snippets to Bulk Tokenize/Delete Tokens Bulk Insert and Bulk Detokenize's README sections each show a code snippet iterating the response; Bulk Tokenize and Bulk Delete Tokens only had a JSON sample with no matching Java. Added one to each, same pattern: - Bulk Tokenize: outer loop over records, inner loop over each record's tokens (it reports at two levels - see the paragraph directly above). - Bulk Delete Tokens: single loop over records, success/error branch on getError(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3abe887 commit 05ce8d0

9 files changed

Lines changed: 180 additions & 24 deletions

File tree

.cspell.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@
111111
"recordss",
112112
"rarr",
113113
"servname",
114-
"nodename"
114+
"nodename",
115+
"flowdb",
116+
"unrenamed"
115117
],
116118
"languageSettings": [
117119
{

flowvault/README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,18 @@ Sample response:
580580

581581
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.
582582

583+
```java
584+
for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) {
585+
for (TokenizeResponseToken token : record.getTokens()) {
586+
if (token.getError() == null) {
587+
System.out.println(record.getValue() + " -> " + token.getTokenGroupName() + " = " + token.getToken());
588+
} else {
589+
System.out.println(record.getValue() + " -> " + token.getTokenGroupName() + " failed: " + token.getError());
590+
}
591+
}
592+
}
593+
```
594+
583595
# Bulk Detokenize
584596

585597
Detokenize many tokens in one call, optionally overriding the redaction applied per token group via `tokenGroupRedactions`.
@@ -640,7 +652,7 @@ Sample response:
640652
"requestId": null,
641653
"value": "4111111111111111",
642654
"tokenGroupName": "card_number_cg",
643-
"metadata": { "table": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1" },
655+
"metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" },
644656
"httpCode": 200,
645657
"token": "5479-4229-4622-1393",
646658
"error": null
@@ -659,6 +671,16 @@ Sample response:
659671
}
660672
```
661673

674+
`record.getMetadata()` is typed as a `DetokenizeMetadata` with `getSkyflowId()`/`getTableName()` — no casting into the raw map required (`null` on records that errored, same as above):
675+
676+
```java
677+
for (BulkDetokenizeResponseRecord record : detokenizeResponse.getRecords()) {
678+
if (record.getMetadata() != null) {
679+
System.out.println(record.getMetadata().getSkyflowId() + " / " + record.getMetadata().getTableName());
680+
}
681+
}
682+
```
683+
662684
Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting.
663685

664686
# Bulk Delete Tokens
@@ -713,6 +735,16 @@ Sample response:
713735
}
714736
```
715737

738+
```java
739+
for (BulkDeleteTokensResponseRecord record : deleteTokensResponse.getRecords()) {
740+
if (record.getError() == null) {
741+
System.out.println(record.getToken() + " deleted");
742+
} else {
743+
System.out.println(record.getToken() + " failed (" + record.getHttpCode() + "): " + record.getError());
744+
}
745+
}
746+
```
747+
716748
Use `deleteTokensResponse.getTokensToRetry()` to get back only the tokens worth resubmitting.
717749

718750
# Custom Request Headers
1002 Bytes
Binary file not shown.

flowvault/src/main/java/com/skyflow/utils/Utils.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.skyflow.vault.data.BulkDetokenizeRequest;
4242
import com.skyflow.vault.data.BulkDetokenizeResponse;
4343
import com.skyflow.vault.data.BulkDetokenizeResponseRecord;
44+
import com.skyflow.vault.data.DetokenizeMetadata;
4445
import com.skyflow.vault.data.BulkInsertRequest;
4546
import com.skyflow.vault.data.BulkInsertResponse;
4647
import com.skyflow.vault.data.BulkInsertResponseRecord;
@@ -782,14 +783,7 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni
782783
int recordsSize = record.size();
783784
for (int index = 0; index < recordsSize; index++) {
784785
V1FlowDetokenizeResponseObject current = record.get(index);
785-
Map<String, Object> data = null;
786-
if(current.getMetadata().isPresent()){
787-
data = current.getMetadata().get();
788-
if (data.containsKey("skyflowID")) {
789-
Object value = data.remove("skyflowID");
790-
data.put("skyflowId", value);
791-
}
792-
}
786+
DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(current.getMetadata().orElse(null));
793787
String reqID = null;
794788
if(current.getError().isPresent()){
795789
reqID = extractRequestId(headers);
@@ -799,7 +793,7 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni
799793
current.getToken().orElse(null),
800794
current.getValue().orElse(null),
801795
current.getTokenGroupName().orElse(null),
802-
data,
796+
metadata,
803797
current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200),
804798
current.getError().orElse(null),
805799
reqID));

flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
import com.google.gson.Gson;
44

5-
import java.util.Map;
6-
75
// Bulk counterpart of DetokenizeResponseRecord. Adds the caller-facing position of the token
86
// in the submitted payload; all other fields are inherited.
97
public class BulkDetokenizeResponseRecord extends DetokenizeResponseRecord {
108
private final int index;
119
private final String requestId;
1210

1311
public BulkDetokenizeResponseRecord(int index, String token, Object value, String tokenGroupName,
14-
Map<String, Object> metadata, int httpCode, String error,
12+
DetokenizeMetadata metadata, int httpCode, String error,
1513
String requestId) {
1614
super(token, value, tokenGroupName, metadata, httpCode, error);
1715
this.index = index;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.skyflow.vault.data;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.annotations.Expose;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Typed shape of a detokenize record's {@code metadata}. The wire response nests the record's
10+
* skyflow id and table name here, under the literal keys {@code skyflowID} and {@code table}
11+
* (see {@code flowdb_dp_apis.proto}) — {@link #parseMetadata(Map)} normalizes both into the
12+
* camelCase, {@code tableName}-shaped accessors below.
13+
*/
14+
public class DetokenizeMetadata {
15+
@Expose(serialize = true)
16+
private final String skyflowId;
17+
@Expose(serialize = true)
18+
private final String tableName;
19+
20+
public DetokenizeMetadata(String skyflowId, String tableName) {
21+
this.skyflowId = skyflowId;
22+
this.tableName = tableName;
23+
}
24+
25+
public String getSkyflowId() {
26+
return skyflowId;
27+
}
28+
29+
public String getTableName() {
30+
return tableName;
31+
}
32+
33+
@Override
34+
public String toString() {
35+
Gson gson = new Gson().newBuilder().serializeNulls().create();
36+
return gson.toJson(this);
37+
}
38+
39+
/**
40+
* Parses the raw wire-shaped metadata map (keys {@code skyflowID}/{@code skyflowId} and
41+
* {@code table}/{@code tableName} — the API has been observed to send either casing) into a
42+
* {@link DetokenizeMetadata}. Returns {@code null} for {@code null} input, matching the
43+
* record-level metadata field being absent entirely on error records.
44+
*/
45+
public static DetokenizeMetadata parseMetadata(Map<String, Object> rawMetadata) {
46+
if (rawMetadata == null) {
47+
return null;
48+
}
49+
Object skyflowId = rawMetadata.containsKey("skyflowId") ? rawMetadata.get("skyflowId") : rawMetadata.get("skyflowID");
50+
Object tableName = rawMetadata.containsKey("tableName") ? rawMetadata.get("tableName") : rawMetadata.get("table");
51+
return new DetokenizeMetadata(
52+
skyflowId != null ? skyflowId.toString() : null,
53+
tableName != null ? tableName.toString() : null);
54+
}
55+
}

flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package com.skyflow.vault.data;
22

3-
import java.util.Map;
4-
53
public class DetokenizeResponseRecord extends BaseDetokenizeRecordResponse {
64
// Passed straight through from V1FlowDetokenizeResponseObject.getValue() (Optional<Object>).
75
private final Object value;
86
private final String tokenGroupName;
9-
private final Map<String, Object> metadata;
7+
private final DetokenizeMetadata metadata;
108
private final int httpCode;
119

1210
public DetokenizeResponseRecord(String token, Object value, String tokenGroupName,
13-
Map<String, Object> metadata, int httpCode, String error) {
11+
DetokenizeMetadata metadata, int httpCode, String error) {
1412
super(token, error);
1513
this.value = value;
1614
this.tokenGroupName = tokenGroupName;
@@ -26,7 +24,13 @@ public String getTokenGroupName() {
2624
return tokenGroupName;
2725
}
2826

29-
public Map<String, Object> getMetadata() {
27+
/**
28+
* The record's skyflowId/tableName, typed. The API models this generically (see
29+
* {@link DetokenizeMetadata#parseMetadata(java.util.Map)}), but the SDK parses it here so
30+
* callers get {@link DetokenizeMetadata#getSkyflowId()}/{@link DetokenizeMetadata#getTableName()}
31+
* directly, with no casting required.
32+
*/
33+
public DetokenizeMetadata getMetadata() {
3034
return metadata;
3135
}
3236

flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ public void testDetokenizeRequest_defaultsAreNull() {
158158

159159
@Test
160160
public void testDetokenizeResponseRecord_gettersReturnConstructorValues() {
161-
Map<String, Object> metadata = new HashMap<>();
162-
metadata.put("key", "value");
161+
DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1");
163162

164163
DetokenizeResponseRecord response = new DetokenizeResponseRecord(
165164
"tok-1", "secret-value", "group1", metadata, 200, null);

flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* constructor logic or toString() serialization: {@link Token}, {@link TokenizeResponseToken},
1515
* {@link TokenizeResponseRecord}, {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary},
1616
* {@link DeleteTokensRecord}, {@link BulkDeleteTokensResponseRecord},
17-
* {@link DeleteTokensSummary}, {@link DetokenizeSummary},
17+
* {@link DeleteTokensSummary}, {@link DetokenizeSummary}, {@link DetokenizeMetadata},
1818
* {@link ErrorRecord} and {@link DetokenizeResponseObject}.
1919
*/
2020
public class ResponseComponentTests {
@@ -531,8 +531,7 @@ public void testErrorRecord_toStringNotNull() {
531531

532532
@Test
533533
public void testBulkDetokenizeResponseRecord_gettersReturnConstructorValues() {
534-
Map<String, Object> metadata = new HashMap<>();
535-
metadata.put("key", "value");
534+
DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1");
536535

537536
BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord(
538537
4, "tok-1", "secret-value", "group1", metadata, 200, null, null);
@@ -575,4 +574,77 @@ public void testBulkDetokenizeResponseRecord_toStringSerializesNulls() {
575574
Assert.assertTrue(json.contains("\"token\":\"tok\""));
576575
Assert.assertTrue(json.contains("\"error\":null"));
577576
}
577+
578+
// ── DetokenizeMetadata ────────────────────────────────────────────────────
579+
580+
@Test
581+
public void testDetokenizeMetadata_gettersReturnConstructorValues() {
582+
DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1");
583+
584+
Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId());
585+
Assert.assertEquals("table1", metadata.getTableName());
586+
}
587+
588+
@Test
589+
public void testDetokenizeMetadata_toStringSerializesFields() {
590+
String json = new DetokenizeMetadata("skyflow-id-1", "table1").toString();
591+
592+
Assert.assertTrue(json.contains("\"skyflowId\":\"skyflow-id-1\""));
593+
Assert.assertTrue(json.contains("\"tableName\":\"table1\""));
594+
}
595+
596+
@Test
597+
public void testParseMetadata_returnsNullWhenRawMetadataIsNull() {
598+
Assert.assertNull(DetokenizeMetadata.parseMetadata(null));
599+
}
600+
601+
@Test
602+
public void testParseMetadata_parsesTheCamelCaseWireShape() {
603+
// The shape Utils.formatBulkDetokenizeResponse actually hands this after its own
604+
// skyflowID -> skyflowId rename; table is left as-is on the wire.
605+
Map<String, Object> raw = new HashMap<>();
606+
raw.put("skyflowId", "skyflow-id-1");
607+
raw.put("table", "table1");
608+
609+
DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw);
610+
611+
Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId());
612+
Assert.assertEquals("table1", metadata.getTableName());
613+
}
614+
615+
@Test
616+
public void testParseMetadata_prefersAnAlreadyCamelCasedTableNameKeyOverTable() {
617+
// Exercises the containsKey("tableName") branch directly - every other test only ever
618+
// supplies the wire's "table" key, never "tableName" itself.
619+
Map<String, Object> raw = new HashMap<>();
620+
raw.put("skyflowId", "skyflow-id-1");
621+
raw.put("tableName", "table1");
622+
raw.put("table", "should-be-ignored");
623+
624+
DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw);
625+
626+
Assert.assertEquals("table1", metadata.getTableName());
627+
}
628+
629+
@Test
630+
public void testParseMetadata_parsesTheLiteralProtoWireShape() {
631+
// flowdb_dp_apis.proto's own example value uses this exact casing/naming -
632+
// {"table": "table1", "skyflowID": "..."} - unrenamed.
633+
Map<String, Object> raw = new HashMap<>();
634+
raw.put("skyflowID", "skyflow-id-1");
635+
raw.put("table", "table1");
636+
637+
DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw);
638+
639+
Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId());
640+
Assert.assertEquals("table1", metadata.getTableName());
641+
}
642+
643+
@Test
644+
public void testParseMetadata_missingKeysParseAsNull() {
645+
DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(new HashMap<>());
646+
647+
Assert.assertNull(metadata.getSkyflowId());
648+
Assert.assertNull(metadata.getTableName());
649+
}
578650
}

0 commit comments

Comments
 (0)