Skip to content

Commit

Permalink
Merge pull request #102 from jpsphaxer/allow-custom-header-to-path-co…
Browse files Browse the repository at this point in the history
…nfig-request

Allow custom headers to path config request (Issue 100)
  • Loading branch information
jayohms authored Feb 26, 2025
2 parents 07173f6 + cbf2c7b commit 123f42b
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 27 deletions.
12 changes: 10 additions & 2 deletions core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ object Hotwire {
/**
* Loads the [PathConfiguration] JSON file(s) from the provided location to
* configure navigation rules.
* @param context The application or activity context.
* @param location Specifies local and/or remote location to retrieve path configuration files.
* @param clientConfig Optional HTTP client configuration options to use when fetching remote
* path configuration files from your server.
*/
fun loadPathConfiguration(context: Context, location: PathConfiguration.Location) {
config.pathConfiguration.load(context.applicationContext, location)
fun loadPathConfiguration(
context: Context,
location: PathConfiguration.Location,
clientConfig: PathConfiguration.ClientConfig = PathConfiguration.ClientConfig()
) {
config.pathConfiguration.load(context.applicationContext, location, clientConfig)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,31 @@ class PathConfiguration {
val remoteFileUrl: String? = null
)

/**
* HTTP client configuration options when fetching remote path configuration
* files from your server.
*/
data class ClientConfig(
/**
* Custom headers to send with each remote path configuration file request.
*/
val headers: Map<String, String> = emptyMap()
)

/**
* Loads and parses the specified configuration file(s) from their local
* and/or remote locations.
*/
fun load(context: Context, location: Location) {
fun load(
context: Context,
location: Location,
clientConfig: ClientConfig
) {
if (loader == null) {
loader = PathConfigurationLoader(context.applicationContext)
}

loader?.load(location) {
loader?.load(location, clientConfig) {
cachedProperties.clear()
rules = it.rules
settings = it.settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,30 @@ internal class PathConfigurationLoader(val context: Context) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = dispatcherProvider.io + Job()

fun load(location: PathConfiguration.Location, onCompletion: (PathConfiguration) -> Unit) {
fun load(
location: PathConfiguration.Location,
clientConfig: PathConfiguration.ClientConfig,
onCompletion: (PathConfiguration) -> Unit
) {
location.assetFilePath?.let {
loadBundledAssetConfiguration(it, onCompletion)
}

location.remoteFileUrl?.let {
downloadRemoteConfiguration(it, onCompletion)
downloadRemoteConfiguration(it, clientConfig, onCompletion)
}
}

private fun downloadRemoteConfiguration(url: String, onCompletion: (PathConfiguration) -> Unit) {
private fun downloadRemoteConfiguration(
url: String,
clientConfig: PathConfiguration.ClientConfig,
onCompletion: (PathConfiguration) -> Unit
) {
// Always load the previously cached version first, if available
loadCachedConfigurationForUrl(url, onCompletion)

launch {
repository.getRemoteConfiguration(url)?.let { json ->
repository.getRemoteConfiguration(url, clientConfig)?.let { json ->
load(json)?.let {
logEvent("remotePathConfigurationLoaded", url)
onCompletion(it)
Expand All @@ -42,15 +50,21 @@ internal class PathConfigurationLoader(val context: Context) : CoroutineScope {
}
}

private fun loadBundledAssetConfiguration(filePath: String, onCompletion: (PathConfiguration) -> Unit) {
private fun loadBundledAssetConfiguration(
filePath: String,
onCompletion: (PathConfiguration) -> Unit
) {
val json = repository.getBundledConfiguration(context, filePath)
load(json)?.let {
logEvent("bundledPathConfigurationLoaded", filePath)
onCompletion(it)
}
}

private fun loadCachedConfigurationForUrl(url: String, onCompletion: (PathConfiguration) -> Unit) {
private fun loadCachedConfigurationForUrl(
url: String,
onCompletion: (PathConfiguration) -> Unit
) {
repository.getCachedConfigurationForUrl(context, url)?.let { json ->
load(json)?.let {
logEvent("cachedPathConfigurationLoaded", url)
Expand All @@ -59,7 +73,10 @@ internal class PathConfigurationLoader(val context: Context) : CoroutineScope {
}
}

private fun cacheConfigurationForUrl(url: String, pathConfiguration: PathConfiguration) {
private fun cacheConfigurationForUrl(
url: String,
pathConfiguration: PathConfiguration
) {
repository.cacheConfigurationForUrl(context, url, pathConfiguration)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,42 @@ import okhttp3.Request
internal class PathConfigurationRepository {
private val cacheFile = "turbo"

suspend fun getRemoteConfiguration(url: String): String? {
val request = Request.Builder().url(url).build()
suspend fun getRemoteConfiguration(
url: String,
clientConfig: PathConfiguration.ClientConfig
): String? {
val requestBuilder = Request.Builder().url(url)

clientConfig.headers.forEach { (key, value) ->
requestBuilder.header(key, value)
}

val request = requestBuilder.build()

return withContext(dispatcherProvider.io) {
issueRequest(request)
}
}

fun getBundledConfiguration(context: Context, filePath: String): String {
fun getBundledConfiguration(
context: Context,
filePath: String
): String {
return contentFromAsset(context, filePath)
}

fun getCachedConfigurationForUrl(context: Context, url: String): String? {
fun getCachedConfigurationForUrl(
context: Context,
url: String
): String? {
return prefs(context).getString(url, null)
}

fun cacheConfigurationForUrl(context: Context, url: String, pathConfiguration: PathConfiguration) {
fun cacheConfigurationForUrl(
context: Context,
url: String,
pathConfiguration: PathConfiguration
) {
prefs(context).edit {
putString(url, pathConfiguration.toJson())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit

@ExperimentalCoroutinesApi
open class BaseRepositoryTest : BaseUnitTest() {
private val server = MockWebServer()
internal val server = MockWebServer()
private val testDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler())

override fun setup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.google.gson.reflect.TypeToken
import dev.hotwire.core.turbo.BaseRepositoryTest
import dev.hotwire.core.turbo.http.HotwireHttpClient
import dev.hotwire.core.turbo.config.PathConfiguration.*
import dev.hotwire.core.turbo.util.toObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -35,7 +35,7 @@ class PathConfigurationRepositoryTest : BaseRepositoryTest() {

runBlocking {
launch(Dispatchers.Main) {
val json = repository.getRemoteConfiguration(baseUrl())
val json = repository.getRemoteConfiguration(baseUrl(), ClientConfig())
assertThat(json).isNotNull()

val config = load(json)
Expand Down Expand Up @@ -66,6 +66,28 @@ class PathConfigurationRepositoryTest : BaseRepositoryTest() {
assertThat(cachedConfig?.rules?.size).isEqualTo(1)
}

@Test
fun `getRemoteConfiguration should include custom headers`() {
enqueueResponse("test-configuration.json")

val clientConfig = ClientConfig(
headers = mapOf(
"Accept" to "application/json",
"Custom-Header" to "test-value"
)
)

runBlocking {
launch(Dispatchers.Main) {
repository.getRemoteConfiguration(baseUrl(), clientConfig)

val request = server.takeRequest()
assertThat(request.headers["Custom-Header"]).isEqualTo("test-value")
assertThat(request.headers["Accept"]).isEqualTo("application/json")
}
}
}

private fun load(json: String?): PathConfiguration? {
return json?.toObject(object : TypeToken<PathConfiguration>() {})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockito_kotlin.*
import dev.hotwire.core.turbo.BaseRepositoryTest
import dev.hotwire.core.turbo.config.PathConfiguration.ClientConfig
import dev.hotwire.core.turbo.config.PathConfiguration.Location
import dev.hotwire.core.turbo.nav.PresentationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -24,14 +25,24 @@ class PathConfigurationTest : BaseRepositoryTest() {
private lateinit var pathConfiguration: PathConfiguration
private val mockRepository = mock<PathConfigurationRepository>()
private val url = "https://turbo.hotwired.dev"
private val clientConfig = ClientConfig(
headers = mapOf(
"Accept" to "application/json",
"Custom-Header" to "test-value"
)
)

@Before
override fun setup() {
super.setup()

context = ApplicationProvider.getApplicationContext()
pathConfiguration = PathConfiguration().apply {
load(context, Location(assetFilePath = "json/test-configuration.json"))
load(
context = context,
location = Location(assetFilePath = "json/test-configuration.json"),
clientConfig = clientConfig
)
}
}

Expand Down Expand Up @@ -59,10 +70,11 @@ class PathConfigurationTest : BaseRepositoryTest() {
runBlocking {
val remoteUrl = "$url/demo/configurations/android-v1.json"
val location = Location(remoteFileUrl = remoteUrl)
val clientConfig = PathConfiguration.ClientConfig()

pathConfiguration.load(context, location)
pathConfiguration.load(context, location, clientConfig)
verify(mockRepository).getCachedConfigurationForUrl(context, remoteUrl)
verify(mockRepository).getRemoteConfiguration(remoteUrl)
verify(mockRepository).getRemoteConfiguration(remoteUrl, clientConfig)
}
}

Expand All @@ -75,11 +87,13 @@ class PathConfigurationTest : BaseRepositoryTest() {
runBlocking {
val remoteUrl = "$url/demo/configurations/android-v1.json"
val location = Location(remoteFileUrl = remoteUrl)
val clientConfig = PathConfiguration.ClientConfig()
val json = """{ "settings": {}, "rules": [] }"""

whenever(mockRepository.getRemoteConfiguration(remoteUrl)).thenReturn(json)
whenever(mockRepository.getRemoteConfiguration(remoteUrl, clientConfig))
.thenReturn(json)

pathConfiguration.load(context, location)
pathConfiguration.load(context, location, clientConfig)
verify(mockRepository).cacheConfigurationForUrl(eq(context), eq(remoteUrl), any())
}
}
Expand All @@ -93,11 +107,13 @@ class PathConfigurationTest : BaseRepositoryTest() {
runBlocking {
val remoteUrl = "$url/demo/configurations/android-v1.json"
val location = Location(remoteFileUrl = remoteUrl)
val clientConfig = PathConfiguration.ClientConfig()
val json = "malformed-json"

whenever(mockRepository.getRemoteConfiguration(remoteUrl)).thenReturn(json)
whenever(mockRepository.getRemoteConfiguration(remoteUrl, clientConfig))
.thenReturn(json)

pathConfiguration.load(context, location)
pathConfiguration.load(context, location, clientConfig)
verify(mockRepository, never()).cacheConfigurationForUrl(any(), any(), any())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.navigation.testing.TestNavHostController
import androidx.navigation.ui.R
import androidx.test.core.app.ApplicationProvider
import dev.hotwire.core.turbo.config.PathConfiguration
import dev.hotwire.core.turbo.config.PathConfiguration.Location
import dev.hotwire.core.turbo.nav.*
import dev.hotwire.core.turbo.visit.VisitOptions
import org.assertj.core.api.Assertions.assertThat
Expand Down Expand Up @@ -68,7 +69,11 @@ class NavigatorRuleTest {
context = ApplicationProvider.getApplicationContext()
controller = buildControllerWithGraph()
pathConfiguration = PathConfiguration().apply {
load(context, PathConfiguration.Location(assetFilePath = "json/test-configuration.json"))
load(
context = context,
location = Location(assetFilePath = "json/test-configuration.json"),
clientConfig = PathConfiguration.ClientConfig()
)
}
}

Expand Down

0 comments on commit 123f42b

Please sign in to comment.