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 #56

Merged
merged 22 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f944b6e
feat: Support transient identities and traits
novakzaballa Aug 13, 2024
fb26329
New changes transient
novakzaballa Aug 22, 2024
cedfb8c
Refact transient traits/identities
novakzaballa Aug 23, 2024
d8f123d
Solve code issues
novakzaballa Aug 23, 2024
7f82cbc
Solve test issues
novakzaballa Aug 26, 2024
a5d39aa
Merge remote-tracking branch 'origin' into feat/support-transient-ide…
matthewelwell Oct 4, 2024
986f1de
Revert unnecessary changes to setTrait and setTraits
matthewelwell Oct 4, 2024
61af2ed
Revert unnecessary changes to trait tests
matthewelwell Oct 4, 2024
0a8433c
Move identity tests to their own test file
matthewelwell Oct 4, 2024
0f31f7f
Remove setTraits test with transiency
matthewelwell Oct 4, 2024
40c6d31
Revert unnecessary changes to getTrait(s)
matthewelwell Oct 4, 2024
5b5bb89
booleans don't need to be nullable
matthewelwell Oct 4, 2024
aed8b4c
Revert unnecessary formatting change
matthewelwell Oct 4, 2024
fbf080c
Remove unnecessary formatting change
matthewelwell Oct 4, 2024
6fd0837
More instances of unncessary null booleans
matthewelwell Oct 4, 2024
608f43d
Final nullable boolean removal
matthewelwell Oct 4, 2024
dbb4c07
Remove unnecessary mock response
matthewelwell Oct 4, 2024
2d62268
Fix compilation error
matthewelwell Oct 4, 2024
95b6de0
Minor changes to mock responses
matthewelwell Oct 4, 2024
8c0b69a
Remove duplicate assert
matthewelwell Oct 4, 2024
f939fba
Revert changes to entities
matthewelwell Oct 4, 2024
23cf1e9
Add more tests
matthewelwell Oct 4, 2024
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
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
)
}
}
}
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading