From dd28aaeb51cfcea5ed9089483d61fffac2d46b0c Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Mon, 9 Nov 2026 18:51:02 -0700 Subject: [PATCH] BREAKING: Restrict Custom identifiers to c / cN and add OptableVID OptableIdentifier.Custom took any key and sent "key:value". Match the iOS SDK, which only allows the custom types "c" and "c1".."cN": - Custom(key: String, value) is replaced by Custom(index: Int?, value) and Custom(value). A null or 0 index is sent as "c", any other as "c". The value is trimmed, as before. - The Optable visitor ID, previously Custom("v", value), is now OptableVID(value), sent as "v" with whitespace removed. Existing Custom(key, value) calls no longer compile, so this is for 2.0.0. Co-Authored-By: Claude Opus 5.5 --- README.md | 14 +++++---- .../java/co/optable/sdk/OptableIdentifier.kt | 16 +++++++--- .../co/optable/sdk/core/IdentifiersEncoder.kt | 11 +++---- .../co/optable/sdk/OptableIdentifiersTest.kt | 29 ++++++++++++++----- .../sdk/core/IdentifiersEncoderTest.kt | 10 +++---- 5 files changed, 52 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3ea430a..b8fd5c8 100644 --- a/README.md +++ b/README.md @@ -237,8 +237,9 @@ val ids = listOf( OptableIdentifier.NetId("netid123"), OptableIdentifier.ID5("id5token"), OptableIdentifier.Utiq("utiqvalue"), - OptableIdentifier.Custom("v", "vidvalue"), - OptableIdentifier.Custom("c1", "custom one"), + OptableIdentifier.OptableVID("vidvalue"), + OptableIdentifier.Custom("custom"), + OptableIdentifier.Custom(1, "custom one"), OptableIdentifier.Raw("raw:alreadyEncoded"), OptableIdentifier.Raw("ultra:someRawValue") ) @@ -264,8 +265,9 @@ List ids = Arrays.asList( new OptableIdentifier.NetId("netid123"), new OptableIdentifier.ID5("id5token"), new OptableIdentifier.Utiq("utiqvalue"), - new OptableIdentifier.Custom("v", "vidvalue"), - new OptableIdentifier.Custom("c1", "custom one"), + new OptableIdentifier.OptableVID("vidvalue"), + new OptableIdentifier.Custom("custom"), + new OptableIdentifier.Custom(1, "custom one"), new OptableIdentifier.Raw("raw:alreadyEncoded"), new OptableIdentifier.Raw("ultra:someRawValue") ); @@ -439,7 +441,7 @@ val hids = listOf( OptableIdentifier.IPv6("2001:0db8:5b96:0000:0000:426f:8e17:642a"), OptableIdentifier.PhoneNumber("+1(555)1234567"), OptableIdentifier.Email("user@example.com"), - OptableIdentifier.Custom("c1", "puid-value"), + OptableIdentifier.Custom(1, "puid-value"), ) optable.targeting(ids, hids) { result -> when (result) { @@ -461,7 +463,7 @@ List hids = Arrays.asList( new OptableIdentifier.IPv6("2001:0db8:5b96:0000:0000:426f:8e17:642a"), new OptableIdentifier.PhoneNumber("+1(555)1234567"), new OptableIdentifier.Email("user@example.com"), - new OptableIdentifier.Custom("c1", "puid-value") + new OptableIdentifier.Custom(1, "puid-value") ); optable.targeting(ids, hids, result -> { if (result instanceof OptableResult.Success success) { diff --git a/android_sdk/src/main/java/co/optable/sdk/OptableIdentifier.kt b/android_sdk/src/main/java/co/optable/sdk/OptableIdentifier.kt index be5a496..ee68ed9 100644 --- a/android_sdk/src/main/java/co/optable/sdk/OptableIdentifier.kt +++ b/android_sdk/src/main/java/co/optable/sdk/OptableIdentifier.kt @@ -99,11 +99,19 @@ sealed class OptableIdentifier { data class Utiq(val value: String) : OptableIdentifier() /** - * Additional custom identifier. - * The `key` represents the identifier type. - * Encoding: for key "v" whitespace removed; otherwise trimmed. + * Optable visitor ID. + * Encoding: whitespace removed. + */ + data class OptableVID(val value: String) : OptableIdentifier() + + /** + * Publisher-provided custom identifier, sent with type "c" when `index` is null or 0, or "c1".."cN" otherwise + * (the absolute value of `index` is used). + * Encoding: trimmed. */ - data class Custom(val key: String, val value: String) : OptableIdentifier() + data class Custom(val index: Int?, val value: String) : OptableIdentifier() { + constructor(value: String) : this(null, value) + } /** * Pre-encoded EID string to include as-is. diff --git a/android_sdk/src/main/java/co/optable/sdk/core/IdentifiersEncoder.kt b/android_sdk/src/main/java/co/optable/sdk/core/IdentifiersEncoder.kt index 1564edb..e1b0271 100644 --- a/android_sdk/src/main/java/co/optable/sdk/core/IdentifiersEncoder.kt +++ b/android_sdk/src/main/java/co/optable/sdk/core/IdentifiersEncoder.kt @@ -5,6 +5,7 @@ import androidx.core.net.toUri import co.optable.sdk.OptableIdentifier import java.security.MessageDigest import java.util.* +import kotlin.math.abs /** * Utility object responsible for encoding raw identifiers into Optable EIDs. @@ -28,6 +29,7 @@ internal class IdentifiersEncoder( private const val ID5 = "id5" private const val UTIQ = "utiq" private const val VID = "v" + private const val CUSTOM = "c" } @@ -55,6 +57,7 @@ internal class IdentifiersEncoder( is OptableIdentifier.NetId -> result.addIfNotNull(NETID, identifier.value, ::removeWhitespaces) is OptableIdentifier.ID5 -> result.addIfNotNull(ID5, identifier.value, ::removeWhitespaces) is OptableIdentifier.Utiq -> result.addIfNotNull(UTIQ, identifier.value, ::normalize) + is OptableIdentifier.OptableVID -> result.addIfNotNull(VID, identifier.value, ::removeWhitespaces) is OptableIdentifier.GoogleGaid -> { if (normalize(googleAdId ?: "") == normalize(identifier.value)) { @@ -64,11 +67,9 @@ internal class IdentifiersEncoder( } is OptableIdentifier.Custom -> { - if (identifier.key == VID) { - result.addIfNotNull(identifier.key, identifier.value, ::removeWhitespaces) - } else { - result.addIfNotNull(identifier.key, identifier.value, ::trim) - } + val index = identifier.index + val key = if (index == null || index == 0) CUSTOM else "$CUSTOM${abs(index)}" + result.addIfNotNull(key, identifier.value, ::trim) } is OptableIdentifier.Raw -> result.add(identifier.value) diff --git a/android_sdk/src/test/java/co/optable/sdk/OptableIdentifiersTest.kt b/android_sdk/src/test/java/co/optable/sdk/OptableIdentifiersTest.kt index 96cda77..f27a425 100644 --- a/android_sdk/src/test/java/co/optable/sdk/OptableIdentifiersTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/OptableIdentifiersTest.kt @@ -194,23 +194,36 @@ class OptableIdentifiersTest { } @Test - fun `encode custom vid`() { + fun `encode optable vid`() { val vid = " vid value " - val ids = listOf(OptableIdentifier.Custom("v", vid)) + val ids = listOf(OptableIdentifier.OptableVID(vid)) val actual = identifiersEncoder.encode(ids) val expected = listOf("v:${removeWhitespaces(vid)}") assertEquals(expected, actual) } @Test - fun `encode custom nonVid`() { + fun `encode custom with index`() { val c1 = " custom one " - val ids = listOf(OptableIdentifier.Custom("c1", c1)) + val ids = listOf(OptableIdentifier.Custom(1, c1)) val actual = identifiersEncoder.encode(ids) val expected = listOf("c1:${c1.trim()}") assertEquals(expected, actual) } + @Test + fun `encode custom without index, with index 0 and with a negative index`() { + val ids = listOf( + OptableIdentifier.Custom("plain"), + OptableIdentifier.Custom(null, "null index"), + OptableIdentifier.Custom(0, "zero index"), + OptableIdentifier.Custom(-3, "negative index"), + ) + val actual = identifiersEncoder.encode(ids) + val expected = listOf("c:plain", "c:null index", "c:zero index", "c3:negative index") + assertEquals(expected, actual) + } + @Test fun `encode raw`() { val ids = listOf( @@ -301,8 +314,8 @@ class OptableIdentifiersTest { OptableIdentifier.NetId(netId), OptableIdentifier.ID5(id5), OptableIdentifier.Utiq(utiq), - OptableIdentifier.Custom("v", vid), - OptableIdentifier.Custom("c1", c1), + OptableIdentifier.OptableVID(vid), + OptableIdentifier.Custom(1, c1), OptableIdentifier.Raw(raw1), OptableIdentifier.Raw(raw2) ) @@ -348,8 +361,8 @@ class OptableIdentifiersTest { OptableIdentifier.NetId(" net id 123 "), OptableIdentifier.ID5(" id5 token "), OptableIdentifier.Utiq(" UTIQ VALUE "), - OptableIdentifier.Custom("v", " vid value "), - OptableIdentifier.Custom("c1", " custom one "), + OptableIdentifier.OptableVID(" vid value "), + OptableIdentifier.Custom(1, " custom one "), OptableIdentifier.Raw("raw:alreadyEncoded"), OptableIdentifier.Raw("x:someRawValue") ) diff --git a/android_sdk/src/test/java/co/optable/sdk/core/IdentifiersEncoderTest.kt b/android_sdk/src/test/java/co/optable/sdk/core/IdentifiersEncoderTest.kt index 871b71c..a169c50 100644 --- a/android_sdk/src/test/java/co/optable/sdk/core/IdentifiersEncoderTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/core/IdentifiersEncoderTest.kt @@ -94,17 +94,17 @@ class IdentifiersEncoderTest { val expected = "c:FooBarBAZ-01234#98765.!!!" - assertEquals(expected, encode(OptableIdentifier.Custom("c", "FooBarBAZ-01234#98765.!!!"))) - assertEquals(expected, encode(OptableIdentifier.Custom("c", " FooBarBAZ-01234#98765.!!!"))) - assertEquals(expected, encode(OptableIdentifier.Custom("c", "FooBarBAZ-01234#98765.!!! "))) - assertEquals(expected, encode(OptableIdentifier.Custom("c", " FooBarBAZ-01234#98765.!!! "))) + assertEquals(expected, encode(OptableIdentifier.Custom("FooBarBAZ-01234#98765.!!!"))) + assertEquals(expected, encode(OptableIdentifier.Custom(" FooBarBAZ-01234#98765.!!!"))) + assertEquals(expected, encode(OptableIdentifier.Custom("FooBarBAZ-01234#98765.!!! "))) + assertEquals(expected, encode(OptableIdentifier.Custom(" FooBarBAZ-01234#98765.!!! "))) } @Test fun cid_isCaseSensitive() { val unexpected = "c:FooBarBAZ-01234#98765.!!!" - val actual = encode(OptableIdentifier.Custom("c", "foobarBAZ-01234#98765.!!!")) + val actual = encode(OptableIdentifier.Custom("foobarBAZ-01234#98765.!!!")) assertNotEquals(unexpected, actual) }