Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support transient identities and traits #54

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ class Flagsmith constructor(
const val DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS = 10
}

fun getFeatureFlags(identity: String? = null, result: (Result<List<Flag>>) -> Unit) {
fun getFeatureFlags(identity: String? = null, transient: Boolean? = null, result: (Result<List<Flag>>) -> Unit) {
// Save the last used identity as we'll refresh with this if we get update events
lastUsedIdentity = identity

if (identity != null) {
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult { res ->
retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult { res ->
flagUpdateFlow.tryEmit(res.getOrNull()?.flags ?: emptyList())
result(res.map { it.flags })
}
Expand Down Expand Up @@ -139,40 +139,64 @@ class Flagsmith constructor(
result(res.map { flag -> flag?.featureStateValue })
}

fun getTrait(id: String, identity: String, result: (Result<Trait?>) -> Unit) =
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult { res ->
fun getTrait(
id: String,
identity: String,
transient: Boolean? = null,
result: (Result<Trait?>) -> Unit
) {
retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult { res ->
result(res.map { value -> value.traits.find { it.key == id } })
}.also { lastUsedIdentity = identity }
}

fun getTraits(identity: String, result: (Result<List<Trait>>) -> Unit) =
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult { res ->
fun getTraits(
identity: String,
transient: Boolean? = null,
result: (Result<List<Trait>>) -> Unit
) {
retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult { res ->
result(res.map { it.traits })
}.also { lastUsedIdentity = identity }
}

fun setTrait(trait: Trait, identity: String, result: (Result<TraitWithIdentity>) -> Unit) =
retrofit.postTraits(IdentityAndTraits(identity, listOf(trait)))
fun setTrait(trait: Trait, identity: String, transient: Boolean? = null, result: (Result<TraitWithIdentity>) -> Unit) {
val identityAndTraits = if (transient != null) {
IdentityAndTraits(identity, listOf(trait), transient)
} else {
IdentityAndTraits(identity, listOf(trait))
}
retrofit.postTraits(identityAndTraits)
.enqueueWithResult(result = {
result(it.map { response -> TraitWithIdentity(
key = response.traits.first().key,
traitValue = response.traits.first().traitValue,
identity = Identity(identity)
identity = Identity(identity),
transient = response.traits.first().transient
)})
})
}

fun setTraits(traits: List<Trait>, identity: String, result: (Result<List<TraitWithIdentity>>) -> Unit) {
retrofit.postTraits(IdentityAndTraits(identity, traits)).enqueueWithResult(result = {
fun setTraits(traits: List<Trait>, identity: String, transient: Boolean? = null, result: (Result<List<TraitWithIdentity>>) -> Unit) {
val identityAndTraits = if (transient != null) {
IdentityAndTraits(identity, traits, transient)
} else {
IdentityAndTraits(identity, traits)
}
retrofit.postTraits(identityAndTraits).enqueueWithResult(result = {
result(it.map { response -> response.traits.map { trait ->
TraitWithIdentity(
key = trait.key,
traitValue = trait.traitValue,
identity = Identity(identity)
identity = Identity(identity),
transient = trait.transient
)
}})
})
}

fun getIdentity(identity: String, result: (Result<IdentityFlagsAndTraits>) -> Unit) =
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult(defaults = null, result = result)
fun getIdentity(identity: String, transient: Boolean? = null, result: (Result<IdentityFlagsAndTraits>) -> Unit) =
retrofit.getIdentityFlagsAndTraits(identity, transient).enqueueWithResult(defaults = null, result = result)
.also { lastUsedIdentity = identity }

fun clearCache() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.flagsmith.entities

data class Identity(
val identifier: String
val identifier: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import com.google.gson.annotations.SerializedName

data class IdentityAndTraits(
@SerializedName(value = "identifier") val identifier: String,
@SerializedName(value = "traits") val traits: List<Trait>
@SerializedName(value = "traits") val traits: List<Trait>,
@SerializedName(value = "transient") val transient: Boolean? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package com.flagsmith.entities

data class IdentityFlagsAndTraits(
val flags: ArrayList<Flag>,
val traits: ArrayList<Trait>
val traits: ArrayList<Trait>,
)
38 changes: 20 additions & 18 deletions FlagsmithClient/src/main/java/com/flagsmith/entities/Trait.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ package com.flagsmith.entities

import com.google.gson.annotations.SerializedName


data class Trait (
val identifier: String? = null,
@SerializedName(value = "trait_key") val key: String,
@SerializedName(value = "trait_value") val traitValue: Any
@SerializedName(value = "trait_value") val traitValue: Any,
val transient: Boolean? = null
) {

constructor(key: String, value: String)
: this(key = key, traitValue = value)
constructor(key: String, value: String, transient: Boolean? = null)
: this(key = key, traitValue = value, transient = transient)

constructor(key: String, value: Int)
: this(key = key, traitValue = value)
constructor(key: String, value: Int, transient: Boolean? = null)
: this(key = key, traitValue = value, transient = transient)

constructor(key: String, value: Double)
: this(key = key, traitValue = value)
constructor(key: String, value: Double, transient: Boolean? = null)
: this(key = key, traitValue = value, transient = transient)

constructor(key: String, value: Boolean)
: this(key = key, traitValue = value)
constructor(key: String, value: Boolean, transient: Boolean? = null)
: this(key = key, traitValue = value, transient = transient)

@Deprecated("Use traitValue instead or one of the type-safe getters", ReplaceWith("traitValue"))
val value: String
Expand All @@ -42,25 +44,25 @@ data class Trait (

val booleanValue: Boolean?
get() = traitValue as? Boolean

}

data class TraitWithIdentity (
@SerializedName(value = "trait_key") val key: String,
@SerializedName(value = "trait_value") val traitValue: Any,
val identity: Identity,
val transient: Boolean? = null
) {
constructor(key: String, value: String, identity: Identity)
: this(key = key, traitValue = value, identity = identity)
constructor(key: String, value: String, identity: Identity, transient: Boolean? = null)
: this(key = key, traitValue = value, identity = identity, transient = transient)

constructor(key: String, value: Int, identity: Identity)
: this(key = key, traitValue = value, identity = identity)
constructor(key: String, value: Int, identity: Identity, transient: Boolean? = null)
: this(key = key, traitValue = value, identity = identity, transient = transient)

constructor(key: String, value: Double, identity: Identity)
: this(key = key, traitValue = value, identity = identity)
constructor(key: String, value: Double, identity: Identity, transient: Boolean? = null)
: this(key = key, traitValue = value, identity = identity, transient = transient)

constructor(key: String, value: Boolean, identity: Identity)
: this(key = key, traitValue = value, identity = identity)
constructor(key: String, value: Boolean, identity: Identity, transient: Boolean? = null)
: this(key = key, traitValue = value, identity = identity, transient = transient)

@Deprecated("Use traitValue instead or one of the type-safe getters", ReplaceWith("traitValue"))
val value: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import retrofit2.http.Query
interface FlagsmithRetrofitService {

@GET("identities/")
fun getIdentityFlagsAndTraits(@Query("identifier") identity: String) : Call<IdentityFlagsAndTraits>
fun getIdentityFlagsAndTraits(@Query("identifier") identity: String, @Query("transient") transient: Boolean? = null) : Call<IdentityFlagsAndTraits>

@GET("flags/")
fun getFlags() : Call<List<Flag>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ suspend fun Flagsmith.getTraitSync(id: String, identity: String): Result<Trait?>
suspend fun Flagsmith.setTraitSync(trait: Trait, identity: String) : Result<TraitWithIdentity>
= suspendCoroutine { cont -> this.setTrait(trait, identity) { cont.resume(it) } }

suspend fun Flagsmith.setTraitsSync(traits: List<Trait>, identity: String) : Result<List<TraitWithIdentity>>
= suspendCoroutine { cont -> this.setTraits(traits, identity) { cont.resume(it) } }
suspend fun Flagsmith.setTraitsSync(traits: List<Trait>, identity: String, transient: Boolean = false) : Result<List<TraitWithIdentity>>
= suspendCoroutine { cont -> this.setTraits(traits, identity, transient) { cont.resume(it) } }

suspend fun Flagsmith.getIdentitySync(identity: String): Result<IdentityFlagsAndTraits>
= suspendCoroutine { cont -> this.getIdentity(identity) { cont.resume(it) } }
suspend fun Flagsmith.getIdentitySync(identity: String, transient: Boolean = false): Result<IdentityFlagsAndTraits>
= suspendCoroutine { cont -> this.getIdentity(identity, transient) { cont.resume(it) } }

48 changes: 45 additions & 3 deletions FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,32 @@ class TraitsTests {
}
}

@Test
fun testSetTraitsWithTransient() {
mockServer.mockResponseFor(MockEndpoint.SET_TRANSIENT_TRAITS)
runBlocking {
val result =
flagsmith.setTraitsSync(
listOf(
Trait(
key = "trait-one-with-transient",
value = "transient-trait-one",
transient = true
),
Trait(
key = "trait-two",
value = "trait-two-value",
transient = false
),
), "identity-with-transient-traits")
assertTrue(result.isSuccess)
assertEquals("trait-one-with-transient", result.getOrThrow().first().key)
assertEquals("transient-trait-one", result.getOrThrow().first().stringValue)
assertEquals("identity-with-transient-traits", result.getOrThrow().first().identity.identifier)
assertEquals(true, result.getOrThrow().first().transient)
}
}

@Test
fun testSetTraitInteger() {
mockServer.mockResponseFor(MockEndpoint.SET_TRAIT_INTEGER)
Expand All @@ -122,7 +148,7 @@ class TraitsTests {
mockServer.mockResponseFor(MockEndpoint.SET_TRAIT_DOUBLE)
runBlocking {
val result =
flagsmith.setTraitSync(Trait(key = "set-from-client", value = 0.5), "person")
flagsmith.setTraitSync(Trait(key = "set-from-client", traitValue = 0.5), "person")
assertTrue(result.isSuccess)
assertEquals("set-from-client", result.getOrThrow().key)
assertEquals(0.5, result.getOrThrow().doubleValue)
Expand All @@ -135,7 +161,7 @@ class TraitsTests {
mockServer.mockResponseFor(MockEndpoint.SET_TRAIT_BOOLEAN)
runBlocking {
val result =
flagsmith.setTraitSync(Trait(key = "set-from-client", value = true), "person")
flagsmith.setTraitSync(Trait(key = "set-from-client", traitValue = true), "person")
assertTrue(result.isSuccess)
assertEquals("set-from-client", result.getOrThrow().key)
assertEquals(true, result.getOrThrow().booleanValue)
Expand All @@ -157,4 +183,20 @@ class TraitsTests {
)
}
}
}

@Test
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
fun testGetTransientIdentity() {
mockServer.mockResponseFor(MockEndpoint.GET_TRANSIENT_IDENTITIES)
runBlocking {
val result = flagsmith.getIdentitySync("transient-identity", true)
assertTrue(result.isSuccess)
assertTrue(result.getOrThrow().traits.isNotEmpty())
assertTrue(result.getOrThrow().flags.isNotEmpty())
assertEquals(
"electric pink",
result.getOrThrow().traits.find { trait -> trait.key == "favourite-colour" }?.stringValue
)
assertEquals(true, result.getOrThrow().flags.isNotEmpty())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ enum class MockEndpoint(val path: String, val body: String) {
GET_FLAGS(FlagsEndpoint.path, MockResponses.getFlags),
SET_TRAIT(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTrait),
SET_TRAITS(TraitsBulkEndpoint(listOf(Trait(key = "", traitValue = "")), "").path, MockResponses.setTraits),
SET_TRANSIENT_TRAITS(TraitsBulkEndpoint(listOf(Trait(key = "", traitValue = "")), "").path, MockResponses.setTransientTraits),
GET_TRANSIENT_IDENTITIES(IdentityFlagsAndTraitsEndpoint("").path, MockResponses.getTransientIdentities),
SET_TRAIT_INTEGER(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitInteger),
SET_TRAIT_DOUBLE(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitDouble),
SET_TRAIT_BOOLEAN(TraitsEndpoint(Trait(key = "", traitValue = ""), "").path, MockResponses.setTraitBoolean),
Expand Down Expand Up @@ -126,6 +128,35 @@ object MockResponses {
}
""".trimIndent()

val getTransientIdentities = """
{
"flags": [
{
"feature_state_value": null,
"feature": {
"type": "STANDARD",
"name": "no-value",
"id": 35506
},
"enabled": true
}
],
"traits": [
{
"trait_value": "12345",
"trait_key": "set-from-client",
"transient": false
},
{
"trait_value": "electric pink",
"trait_key": "favourite-colour",
"transient": true
}
],
"transient": true
}
""".trimIndent()

val getFlags = """
[
{
Expand Down Expand Up @@ -171,6 +202,25 @@ object MockResponses {
}
""".trimIndent()

val setTransientTraits = """
{
"identifier": "identity-with-transient-traits",
"flags": [],
"traits": [
{
"trait_value": "transient-trait-one",
"trait_key": "trait-one-with-transient",
"transient": true
},
{
"trait_value": "transient-trait-two",
"trait_key": "trait-two-with-transient",
"transient": false
}
]
}
""".trimIndent()

val setTraits = """
{
"identifier": "person",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.flagsmith.mockResponses.endpoints

import com.flagsmith.entities.IdentityFlagsAndTraits

data class IdentityFlagsAndTraitsEndpoint(private val identity: String) :
data class IdentityFlagsAndTraitsEndpoint(private val identity: String, private val transient: Boolean = false) :
GetEndpoint<IdentityFlagsAndTraits>(
path = "/identities/",
params = listOf("identifier" to identity),
params = listOf("identifier" to identity, "transient" to transient),
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import com.flagsmith.entities.Trait
import com.flagsmith.entities.TraitWithIdentity
import com.google.gson.Gson

data class TraitsEndpoint(private val trait: Trait, private val identity: String) :
data class TraitsEndpoint(private val trait: Trait, private val identity: String, private val transient: Boolean? = null) :
PostEndpoint<TraitWithIdentity>(
path = "/identities/",
body = Gson().toJson(
IdentityAndTraits(
identifier = identity,
traits = listOf(trait)
traits = listOf(trait),
transient = transient,
)
),
)
Loading