Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
710122b
Compiles and test, need to add some tests then get it into a sample app
gazreese Jul 1, 2023
104b318
Should be testing but not getting the errors through Fuse so can't ru…
gazreese Jul 3, 2023
fbddca4
Probably gone as far as I can with 2.x fuel, let's try the 3.x
gazreese Jul 5, 2023
2d31deb
Move to Retrofit - seems to be going well so far, test runs
gazreese Jul 5, 2023
5e259b5
Tidying up, setTrait test not working
gazreese Jul 6, 2023
76942b1
All the tests are passing so will finish the retrofit migration
gazreese Jul 6, 2023
70d21e2
Updated some of the logic and added setTraits
gazreese Jul 6, 2023
049a0f6
Checkpoint commit before trying generic converter
gazreese Jul 6, 2023
e555685
Generics working fine
gazreese Jul 6, 2023
297642d
All passing for flags and such with the new generic caching
gazreese Jul 6, 2023
90622ae
Mostly swapped to Retrofit, now need to do the analytics
gazreese Jul 6, 2023
9536809
Analytics now over to retrofit
gazreese Jul 7, 2023
817b73c
Add caching for the getFlags endpoint
gazreese Jul 7, 2023
7e6d429
Get rid of the last of Fuel
gazreese Jul 7, 2023
ef4674b
Another clear-out and all working fine on the tests
gazreese Jul 7, 2023
3b64e5f
Now using Retrofit cache, remove the old stuff
gazreese Jul 7, 2023
46ee338
Now just using HTTP caching
gazreese Jul 7, 2023
dd6c7ca
Delete the old caching logic
gazreese Jul 7, 2023
a9a74f8
Finishing off, should be done for defaults and caching
gazreese Jul 7, 2023
ffb3cc6
Remove unneeded todo
gazreese Jul 7, 2023
83d058d
Remove some more code
gazreese Jul 7, 2023
94f8167
Move cache configuration to its own data class
gazreese Jul 11, 2023
b480ea4
Tidy up the cache config and the tests
gazreese Jul 11, 2023
a56afe0
Update the comments
gazreese Jul 11, 2023
6d87253
Now covers the caching tests
gazreese Jul 11, 2023
790ce47
Tidy up some more of the tests
gazreese Jul 11, 2023
3aa02ed
Some more tidying up
gazreese Jul 11, 2023
a5200d7
Default to caching disabled
gazreese Jul 11, 2023
cb1542a
Last few PR comments
gazreese Jul 12, 2023
340d4f3
Split the read and write timeout for HTTP
gazreese Jul 12, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ build
local.properties

.idea
FlagsmithClient/cache
9 changes: 7 additions & 2 deletions FlagsmithClient/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,17 @@ android {

dependencies {
implementation("com.google.code.gson:gson:2.10")
implementation("com.github.kittinunf.fuel:fuel:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-gson:2.3.1")

// HTTP Client
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
testImplementation("org.mock-server:mockserver-netty-no-dependencies:5.14.0")

testImplementation("org.awaitility:awaitility-kotlin:4.2.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
}

kover {
Expand Down
46 changes: 25 additions & 21 deletions FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package com.flagsmith

import android.content.Context
import com.flagsmith.endpoints.FlagsEndpoint
import com.flagsmith.endpoints.IdentityFlagsAndTraitsEndpoint
import com.flagsmith.endpoints.TraitsEndpoint
import com.flagsmith.entities.Flag
import com.flagsmith.entities.IdentityFlagsAndTraits
import com.flagsmith.entities.Trait
import com.flagsmith.entities.TraitWithIdentity
import com.flagsmith.entities.*
import com.flagsmith.internal.FlagsmithAnalytics
import com.flagsmith.internal.FlagsmithClient
import com.flagsmith.internal.FlagsmithRetrofitService
import com.flagsmith.internal.enqueueWithResult

/**
* Flagsmith
Expand All @@ -28,26 +23,39 @@ class Flagsmith constructor(
private val baseUrl: String = "https://edge.api.flagsmith.com/api/v1",
private val context: Context? = null,
private val enableAnalytics: Boolean = DEFAULT_ENABLE_ANALYTICS,
private val analyticsFlushPeriod: Int = DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS
private val analyticsFlushPeriod: Int = DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS,
private val cacheConfig: FlagsmithCacheConfig = FlagsmithCacheConfig(),
private val defaultFlags: List<Flag> = emptyList(),
private val requestTimeoutSeconds: Long = 4L,
private val readTimeoutSeconds: Long = 6L,
private val writeTimeoutSeconds: Long = 6L
) {
private val client: FlagsmithClient = FlagsmithClient(baseUrl, environmentKey)
private val retrofit: FlagsmithRetrofitService = FlagsmithRetrofitService.create(
baseUrl = baseUrl, environmentKey = environmentKey, context = context, cacheConfig = cacheConfig,
requestTimeoutSeconds = requestTimeoutSeconds, readTimeoutSeconds = readTimeoutSeconds, writeTimeoutSeconds = writeTimeoutSeconds)
private val analytics: FlagsmithAnalytics? =
if (!enableAnalytics) null
else if (context != null) FlagsmithAnalytics(context, client, analyticsFlushPeriod)
else if (context != null) FlagsmithAnalytics(context, retrofit, analyticsFlushPeriod)
else throw IllegalArgumentException("Flagsmith requires a context to use the analytics feature")

init {
if (cacheConfig.enableCache && context == null) {
throw IllegalArgumentException("Flagsmith requires a context to use the cache feature")
}
}

companion object {
const val DEFAULT_ENABLE_ANALYTICS = true
const val DEFAULT_ANALYTICS_FLUSH_PERIOD_SECONDS = 10
}

fun getFeatureFlags(identity: String? = null, result: (Result<List<Flag>>) -> Unit) {
if (identity != null) {
getIdentityFlagsAndTraits(identity) { res ->
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult { res ->
result(res.map { it.flags })
}
} else {
client.request(FlagsEndpoint, result)
retrofit.getFlags().enqueueWithResult(defaults = defaultFlags, result = result)
}
}

Expand All @@ -68,20 +76,20 @@ class Flagsmith constructor(
}

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

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

fun setTrait(trait: Trait, identity: String, result: (Result<TraitWithIdentity>) -> Unit) =
client.request(TraitsEndpoint(trait = trait, identity = identity), result)
retrofit.postTraits(TraitWithIdentity(trait.key, trait.value, Identity(identity))).enqueueWithResult(result = result)

fun getIdentity(identity: String, result: (Result<IdentityFlagsAndTraits>) -> Unit) =
getIdentityFlagsAndTraits(identity, result)
retrofit.getIdentityFlagsAndTraits(identity).enqueueWithResult(defaults = null, result = result)

private fun getFeatureFlag(
featureId: String,
Expand All @@ -95,8 +103,4 @@ class Flagsmith constructor(
})
}

private fun getIdentityFlagsAndTraits(
identity: String,
result: (Result<IdentityFlagsAndTraits>) -> Unit
) = client.request(IdentityFlagsAndTraitsEndpoint(identity = identity), result)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.flagsmith

data class FlagsmithCacheConfig (
val enableCache: Boolean = false,
val cacheTTLSeconds: Long = 3600L, // Default to 1 hour
val cacheSize: Long = 10L * 1024L * 1024L, // 10 MB
)

This file was deleted.

13 changes: 1 addition & 12 deletions FlagsmithClient/src/main/java/com/flagsmith/entities/Flag.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
package com.flagsmith.entities

import com.flagsmith.internal.Deserializer
import com.flagsmith.internal.fromJson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import java.io.Reader

class FlagListDeserializer: Deserializer<List<Flag>> {
override fun deserialize(reader: Reader): List<Flag>? {
val type = object : TypeToken<ArrayList<Flag>>() {}.type
return reader.fromJson<ArrayList<Flag>?>(type = type)
}
}

data class Flag(
val feature: Feature,
Expand All @@ -27,4 +16,4 @@ data class Feature(
@SerializedName(value = "initial_value") val initialValue: String,
@SerializedName(value = "default_enabled") val defaultEnabled: Boolean,
val type: String
)
)
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
package com.flagsmith.entities

import com.flagsmith.internal.Deserializer
import com.flagsmith.internal.fromJson
import java.io.Reader

class IdentityFlagsAndTraitsDeserializer: Deserializer<IdentityFlagsAndTraits> {
override fun deserialize(reader: Reader): IdentityFlagsAndTraits? =
reader.fromJson(IdentityFlagsAndTraits::class.java)
}

data class IdentityFlagsAndTraits(
val flags: ArrayList<Flag>,
val traits: ArrayList<Trait>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.flagsmith.entities

import com.flagsmith.internal.Deserializer
import com.flagsmith.internal.fromJson

import com.google.gson.annotations.SerializedName
import java.io.Reader

class TraitWithIdentityDeserializer: Deserializer<TraitWithIdentity> {
override fun deserialize(reader: Reader): TraitWithIdentity? =
reader.fromJson(TraitWithIdentity::class.java)
}

data class Trait(
val identifier: String? = null,
@SerializedName(value = "trait_key") val key: String,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.flagsmith.endpoints.AnalyticsEndpoint
import org.json.JSONException
import org.json.JSONObject

class FlagsmithAnalytics constructor(
private val context: Context,
private val client: FlagsmithClient,
private val retrofitService: FlagsmithRetrofitService,
private val flushPeriod: Int
) {
private val applicationContext: Context = context.applicationContext
Expand All @@ -21,9 +20,8 @@ class FlagsmithAnalytics constructor(
private val timerRunnable = object : Runnable {
override fun run() {
if (currentEvents.isNotEmpty()) {
client.request(AnalyticsEndpoint(eventMap = currentEvents)) {
it
.onSuccess { resetMap() }
retrofitService.postAnalytics(currentEvents).enqueueWithResult { result ->
result.onSuccess { resetMap() }
.onFailure { err ->
Log.e(
"FLAGSMITH",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.flagsmith.internal;

import android.content.Context
import android.util.Log
import com.flagsmith.FlagsmithCacheConfig
import com.flagsmith.entities.Flag
import com.flagsmith.entities.IdentityFlagsAndTraits
import com.flagsmith.entities.TraitWithIdentity
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.*
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

interface FlagsmithRetrofitService {

@GET("identities/")
fun getIdentityFlagsAndTraits(@Query("identity") identity: String) : Call<IdentityFlagsAndTraits>

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

@POST("traits/")
fun postTraits(@Body trait: TraitWithIdentity) : Call<TraitWithIdentity>

@POST("analytics/flags/")
fun postAnalytics(@Body eventMap: Map<String, Int?>) : Call<Unit>

companion object {
fun create(
baseUrl: String,
environmentKey: String,
context: Context?,
cacheConfig: FlagsmithCacheConfig,
requestTimeoutSeconds: Long,
readTimeoutSeconds: Long,
writeTimeoutSeconds: Long,
): FlagsmithRetrofitService {
fun cacheControlInterceptor(): Interceptor {
return Interceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control", "public, max-age=${cacheConfig.cacheTTLSeconds}")
.removeHeader("Pragma")
.build()
}
}

fun envKeyInterceptor(environmentKey: String): Interceptor {
return Interceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("X-environment-key", environmentKey)
.build()
chain.proceed(request)
}
}

val client = OkHttpClient.Builder()
.addInterceptor(envKeyInterceptor(environmentKey))
.let { if (cacheConfig.enableCache) it.addNetworkInterceptor(cacheControlInterceptor()) else it }
.callTimeout(requestTimeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(readTimeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(writeTimeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
.cache(if (context != null && cacheConfig.enableCache) okhttp3.Cache(context.cacheDir, cacheConfig.cacheSize) else null)
.build()

val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()

return retrofit.create(FlagsmithRetrofitService::class.java)
}
}
}

// Convert a Retrofit Call to a standard Kotlin Result by extending the Call class
// This avoids having to use the suspend keyword in the FlagsmithClient to break the API
// And also avoids a lot of code duplication
fun <T> Call<T>.enqueueWithResult(defaults: T? = null, result: (Result<T>) -> Unit) {
this.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful && response.body() != null) {
result(Result.success(response.body()!!))
} else {
onFailure(call, HttpException(response))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
// If we've got defaults to return, return them
if (defaults != null) {
result(Result.success(defaults))
} else {
result(Result.failure(t))
}
}
})
}

Loading