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

SDKS-3622 Add Accept-Language Header Support #36

Merged
merged 1 commit into from
Feb 11, 2025
Merged
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
1 change: 1 addition & 0 deletions davinci/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.robolectric)

androidTestImplementation(libs.kotlin.test)
androidTestImplementation(libs.kotlinx.coroutines.test)
Expand Down
34 changes: 34 additions & 0 deletions davinci/src/main/kotlin/com/pingidentity/davinci/DaVinci.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.pingidentity.davinci

import android.os.LocaleList
spetrov marked this conversation as resolved.
Show resolved Hide resolved
import com.pingidentity.davinci.module.NodeTransform
import com.pingidentity.davinci.module.Oidc
import com.pingidentity.davinci.plugin.DaVinci
Expand All @@ -17,6 +18,7 @@ import com.pingidentity.orchestrate.module.CustomHeader
// typealias DaVinciConfig = WorkflowConfig
private const val X_REQUESTED_WITH = "x-requested-with"
private const val X_REQUESTED_PLATFORM = "x-requested-platform"
private const val ACCEPT_LANGUAGE = "Accept-Language"

// Constants for header values
private const val PING_SDK = "ping-sdk"
Expand Down Expand Up @@ -49,6 +51,7 @@ fun DaVinci(block: DaVinciConfig.() -> Unit = {}): DaVinci {
module(CustomHeader) {
header(X_REQUESTED_WITH, PING_SDK)
header(X_REQUESTED_PLATFORM, ANDROID)
header(ACCEPT_LANGUAGE, LocaleList.getDefault().toAcceptLanguage())
}
module(NodeTransform)
//Module cookie has lower priority than Oidc, the Cookie module requires the request Url to be set
Expand All @@ -63,4 +66,35 @@ fun DaVinci(block: DaVinciConfig.() -> Unit = {}): DaVinci {
config.apply(block)

return DaVinci(config)
}

/**
* Function to convert a LocaleList to an Accept-Language header value.
*/
fun LocaleList.toAcceptLanguage(): String {
spetrov marked this conversation as resolved.
Show resolved Hide resolved
if (isEmpty) return ""

val languageTags = mutableListOf<String>()
var currentQValue = 0.9

(0 until size()).forEach { index ->
val locale = this[index]

// Add toLanguageTag version first
if (index == 0) {
languageTags.add(locale.toLanguageTag())
currentQValue = 0.9
} else {
languageTags.add("${locale.toLanguageTag()};q=%.1f".format(currentQValue))
currentQValue -= 0.1
}

// Add language version with next q-value
if (locale.toLanguageTag() != locale.language) {
languageTags.add("${locale.language};q=%.1f".format(currentQValue))
currentQValue -= 0.1
}
}

return languageTags.joinToString(", ")
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ import com.pingidentity.testrail.TestRailWatcher
import io.ktor.http.headers
import org.junit.Rule
import org.junit.rules.TestWatcher
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.BeforeTest
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNull

@RunWith(RobolectricTestRunner::class)
class DaVinciErrorTest {
@JvmField
@Rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Rule
import org.junit.rules.TestWatcher
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand All @@ -56,6 +58,7 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

@RunWith(RobolectricTestRunner::class)
class DaVinciTest {
@JvmField
@Rule
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Ping Identity. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package com.pingidentity.davinci

import android.os.LocaleList
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.util.Locale
import kotlin.test.Test
import kotlin.test.assertEquals

@RunWith(RobolectricTestRunner::class)
class LocaleListExtensionsTest {

@Test
fun `empty locale list returns empty string`() {
val emptyLocaleList = LocaleList()
assertEquals("", emptyLocaleList.toAcceptLanguage())
}

@Test
fun `single locale without script returns language tag`() {
val localeList = LocaleList(Locale("en", "US"))
assertEquals("en-US, en;q=0.9", localeList.toAcceptLanguage())
}

@Test
fun `single locale with different language tag adds both versions`() {
val localeList = LocaleList(Locale.forLanguageTag("zh-Hant-TW"))
assertEquals("zh-Hant-TW, zh;q=0.9", localeList.toAcceptLanguage())
}

@Test
fun `multiple locales are ordered with decreasing q values`() {
val localeList = LocaleList(
Locale("en", "US"),
Locale("es", "ES"),
Locale("fr", "FR")
)
assertEquals(
"en-US, en;q=0.9, es-ES;q=0.8, es;q=0.7, fr-FR;q=0.6, fr;q=0.5",
localeList.toAcceptLanguage()
)
}

@Test
fun `complex locale list with scripts handled correctly`() {
val localeList = LocaleList(
Locale.forLanguageTag("zh-Hant-TW"),
Locale("en", "US"),
Locale.forLanguageTag("zh-Hans-CN")
)
assertEquals(
"zh-Hant-TW, zh;q=0.9, en-US;q=0.8, en;q=0.7, zh-Hans-CN;q=0.6, zh;q=0.5",
localeList.toAcceptLanguage()
)
}

@Test
fun `q values decrease correctly for long lists`() {
val localeList = LocaleList(
Locale("en", "US"),
Locale("fr", "FR"),
Locale("de", "DE"),
Locale("it", "IT"),
Locale("es", "ES")
)
assertEquals(
"en-US, en;q=0.9, fr-FR;q=0.8, fr;q=0.7, de-DE;q=0.6, de;q=0.5, " +
"it-IT;q=0.4, it;q=0.3, es-ES;q=0.2, es;q=0.1",
localeList.toAcceptLanguage()
)
}
}
20 changes: 18 additions & 2 deletions foundation/orchestrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The Workflow engine contains the following states:
### Module

Module allows you to register functions in different state of the workflow. For example, you can register a function
that will be called when the workflow is initialized, when a node is received, when a node is sent, and when
that will be called when the workflow is initialized, when a node is received, when a node is sent, and when
the workflow is started.

<img src="images/functions.png" width="500">
Expand Down Expand Up @@ -133,4 +133,20 @@ val noSession = Module {
request
}
}
```
```

## Module execution order
The SDK allows you to register a module into the ```Workflow``` Engine. It accepts several parameters to control how the module is registered and configured:

- ```priority``` (optional): A numeric value that determines the module's execution order in the registry. Default value is 10. Lower priority values indicate higher precedence - modules with lower numbers will be processed first.
- ```mode``` (optional): Determines how the registration handles existing modules. Default is OVERRIDE. The available modes are:
- ```OVERRIDE```: If a module with the same identifier already exists, the new module will replace it.
- ```APPEND```: The new module will be added to the registry's list and cannot be overridden by future registrations. This ensures the module remains in the workflow permanently.
- ```IGNORE```: If a module with the same identifier already exists, the registration request will be silently ignored, keeping the existing module unchanged.

Here is an example of how to register a module with a specific priority and mode:
```kotlin
module(CustomHeader, priority = 5, mode = OverrideMode.APPEND) {
header("Accept-Language", "zh")
}
```
Loading