Skip to content

Commit

Permalink
Merge pull request #56 from Flagsmith/feat/support-transient-identiti…
Browse files Browse the repository at this point in the history
…es-and-traits

feat: support transient identities and traits
  • Loading branch information
khvn26 authored Oct 10, 2024
2 parents 3294590 + 23cf1e9 commit 1604ed0
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 55 deletions.
10 changes: 5 additions & 5 deletions FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,17 @@ class Flagsmith constructor(
const val DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS = 10
}

fun getFeatureFlags(identity: String? = null, traits: List<Trait>? = null, result: (Result<List<Flag>>) -> Unit) {
fun getFeatureFlags(identity: String? = null, traits: List<Trait>? = null, transient: Boolean = false, 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) {
if (traits != null) {
retrofit.postTraits(IdentityAndTraits(identity, traits)).enqueueWithResult(result = {
retrofit.postTraits(IdentityAndTraits(identity, traits, transient)).enqueueWithResult(result = {
result(it.map { response -> response.flags })
}).also { lastUsedIdentity = identity }
} else {
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 @@ -181,8 +181,8 @@ class Flagsmith constructor(
})
}

fun getIdentity(identity: String, result: (Result<IdentityFlagsAndTraits>) -> Unit) =
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult(defaults = null, result = result)
fun getIdentity(identity: String, transient: Boolean = false, 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
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
)
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 = false
) {

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

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

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

constructor(key: String, value: Boolean)
: this(key = key, traitValue = value)
constructor(key: String, value: Boolean, transient: Boolean = false)
: 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 = false
) {
constructor(key: String, value: String, identity: Identity)
: this(key = key, traitValue = value, identity = identity)
constructor(key: String, value: String, identity: Identity, transient: Boolean = false)
: 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 = false)
: 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 = false)
: 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 = false)
: 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,11 +19,12 @@ 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 = false) : Call<IdentityFlagsAndTraits>

@GET("flags/")
fun getFlags() : Call<List<Flag>>

// todo: rename this function
@POST("identities/")
fun postTraits(@Body identity: IdentityAndTraits) : Call<IdentityFlagsAndTraits>

Expand Down
85 changes: 85 additions & 0 deletions FlagsmithClient/src/test/java/com/flagsmith/FeatureFlagTests.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.flagsmith

import com.flagsmith.entities.Trait
import com.flagsmith.mockResponses.MockEndpoint
import com.flagsmith.mockResponses.MockResponses
import com.flagsmith.mockResponses.mockResponseFor
import kotlinx.coroutines.runBlocking
import org.junit.After
Expand All @@ -13,6 +15,9 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest.request
import org.mockserver.model.HttpResponse.response
import org.mockserver.model.JsonBody.json

class FeatureFlagTests {

Expand Down Expand Up @@ -157,4 +162,84 @@ class FeatureFlagTests {
assertEquals(756.0, found?.featureStateValue)
}
}

@Test
fun testGetFeatureFlagsWithTransientTraits() {
mockServer.`when`(
request()
.withPath("/identities/")
.withMethod("POST")
.withBody(
json(
"""
{
"identifier": "identity",
"traits": [
{
"trait_key": "transient-trait",
"trait_value": "value",
"transient": true
},
{
"trait_key": "persisted-trait",
"trait_value": "value",
"transient": false
}
],
"transient": false
}
""".trimIndent()
)
)
)
.respond(
response()
.withStatusCode(200)
.withBody(MockResponses.getTransientIdentities)
)

runBlocking {
val transientTrait = Trait("transient-trait", "value", true)
val persistedTrait = Trait("persisted-trait", "value", false)
val result = flagsmith.getFeatureFlagsSync(
"identity",
listOf(transientTrait, persistedTrait),
false,
)

assertTrue(result.isSuccess)
}
}

@Test
fun testGetFeatureFlagsWithTransientIdentity() {
mockServer.`when`(
request()
.withPath("/identities/")
.withMethod("POST")
.withBody(
json(
"""
{
"identifier": "identity",
"traits": [],
"transient": true
}
""".trimIndent()
)
)
)
.respond(
response()
.withStatusCode(200)
.withBody(MockResponses.getTransientIdentities)
)

runBlocking {
val result = flagsmith.getFeatureFlagsSync(
"identity", listOf(),true,
)
assertTrue(result.isSuccess)
}
}
}
80 changes: 80 additions & 0 deletions FlagsmithClient/src/test/java/com/flagsmith/IdentityTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.flagsmith

import com.flagsmith.entities.Trait
import com.flagsmith.mockResponses.MockEndpoint
import com.flagsmith.mockResponses.MockResponses
import com.flagsmith.mockResponses.mockResponseFor
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest.request

class IdentityTests {

private lateinit var mockServer: ClientAndServer
private lateinit var flagsmith: Flagsmith

@Before
fun setup() {
mockServer = ClientAndServer.startClientAndServer()
flagsmith = Flagsmith(
environmentKey = "",
baseUrl = "http://localhost:${mockServer.localPort}",
enableAnalytics = false,
cacheConfig = FlagsmithCacheConfig(enableCache = false)
)
}

@After
fun tearDown() {
mockServer.stop()
}

@Test
fun testGetIdentity() {
mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES)
runBlocking {
val result = flagsmith.getIdentitySync("person")

mockServer.verify(
request()
.withPath("/identities/")
.withMethod("GET")
.withQueryStringParameter("identifier", "person")
)

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
)
}
}

@Test
fun testGetTransientIdentity() {
mockServer.mockResponseFor(MockEndpoint.GET_TRANSIENT_IDENTITIES)
runBlocking {
val result = flagsmith.getIdentitySync("transient-identity", true)

mockServer.verify(
request()
.withPath("/identities/")
.withMethod("GET")
.withQueryStringParameter("identifier", "transient-identity")
.withQueryStringParameter("transient", "true")
)

assertTrue(result.isSuccess)
assertTrue(result.getOrThrow().traits.isEmpty())
assertTrue(result.getOrThrow().flags.isNotEmpty())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import kotlin.coroutines.suspendCoroutine
suspend fun Flagsmith.hasFeatureFlagSync(forFeatureId: String, identity: String? = null): Result<Boolean>
= suspendCoroutine { cont -> this.hasFeatureFlag(forFeatureId, identity = identity) { cont.resume(it) } }

suspend fun Flagsmith.getFeatureFlagsSync(identity: String? = null, traits: List<Trait>? = null) : Result<List<Flag>>
= suspendCoroutine { cont -> this.getFeatureFlags(identity = identity, traits = traits) { cont.resume(it) } }
suspend fun Flagsmith.getFeatureFlagsSync(identity: String? = null, traits: List<Trait>? = null, transient: Boolean = false) : Result<List<Flag>>
= suspendCoroutine { cont -> this.getFeatureFlags(identity = identity, traits = traits, transient = transient) { cont.resume(it) } }

suspend fun Flagsmith.getValueForFeatureSync(forFeatureId: String, identity: String? = null): Result<Any?>
= suspendCoroutine { cont -> this.getValueForFeature(forFeatureId, identity = identity) { cont.resume(it) } }
Expand All @@ -28,6 +28,6 @@ suspend fun Flagsmith.setTraitSync(trait: Trait, identity: String) : Result<Trai
suspend fun Flagsmith.setTraitsSync(traits: List<Trait>, identity: String) : Result<List<TraitWithIdentity>>
= suspendCoroutine { cont -> this.setTraits(traits, identity) { 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) } }

17 changes: 1 addition & 16 deletions FlagsmithClient/src/test/java/com/flagsmith/TraitsTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,4 @@ class TraitsTests {
assertEquals("person", result.getOrThrow().identity.identifier)
}
}

@Test
fun testGetIdentity() {
mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES)
runBlocking {
val result = flagsmith.getIdentitySync("person")
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
)
}
}
}
}
Loading

0 comments on commit 1604ed0

Please sign in to comment.