diff --git a/.fleet/receipt.json b/.fleet/receipt.json new file mode 100644 index 00000000..212afb24 --- /dev/null +++ b/.fleet/receipt.json @@ -0,0 +1,33 @@ +{ + "spec": { + "template_id": "kmt", + "targets": { + "android": { + "ui": [ + "compose" + ] + }, + "ios": { + "ui": [ + "compose" + ] + }, + "desktop": { + "ui": [ + "compose" + ] + }, + "web": { + "ui": [ + "compose" + ] + }, + "server": { + "engine": [ + "ktor" + ] + } + } + }, + "timestamp": "2024-05-31T15:12:19.991061605Z" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9acc12e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: Run CI +on: + push: + workflow_dispatch: + +jobs: + gradle: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: adopt-hotspot + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + - name: Grant execute permission for Gradlew (Linux/Mac) + if: runner.os != 'Windows' + run: chmod +x ./gradlew + + - name: Execute Gradle build + run: ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0e9e76a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ +local.properties +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +/build-tools/ +/platforms/ +/platform-tools/ +/.temp/ diff --git a/.knownPackages b/.knownPackages new file mode 100644 index 00000000..0ad0d522 --- /dev/null +++ b/.knownPackages @@ -0,0 +1 @@ +r©¿FÊ#óÜ–× '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..93e3f59f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/licenses/android-googletv-license b/licenses/android-googletv-license new file mode 100644 index 00000000..07d43f0b --- /dev/null +++ b/licenses/android-googletv-license @@ -0,0 +1,2 @@ + +601085b94cd77f0b54ff86406957099ebe79c4d6 \ No newline at end of file diff --git a/licenses/android-sdk-arm-dbt-license b/licenses/android-sdk-arm-dbt-license new file mode 100644 index 00000000..0dc9cc07 --- /dev/null +++ b/licenses/android-sdk-arm-dbt-license @@ -0,0 +1,2 @@ + +859f317696f67ef3d7f30a50a5560e7834b43903 \ No newline at end of file diff --git a/licenses/android-sdk-license b/licenses/android-sdk-license new file mode 100644 index 00000000..b8d7962a --- /dev/null +++ b/licenses/android-sdk-license @@ -0,0 +1,2 @@ + +24333f8a63b6825ea9c5514f83c2829b004d1fee \ No newline at end of file diff --git a/licenses/android-sdk-preview-license b/licenses/android-sdk-preview-license new file mode 100644 index 00000000..da4552d2 --- /dev/null +++ b/licenses/android-sdk-preview-license @@ -0,0 +1,2 @@ + +84831b9409646a918e30573bab4c9c91346d8abd \ No newline at end of file diff --git a/licenses/google-gdk-license b/licenses/google-gdk-license new file mode 100644 index 00000000..db3b42fd --- /dev/null +++ b/licenses/google-gdk-license @@ -0,0 +1,2 @@ + +33b6a2b64607f11b759f320ef9dff4ae5c47d97a \ No newline at end of file diff --git a/licenses/intel-android-extra-license b/licenses/intel-android-extra-license new file mode 100644 index 00000000..f82e65b6 --- /dev/null +++ b/licenses/intel-android-extra-license @@ -0,0 +1,2 @@ + +d975f751698a77b662f1254ddbeed3901e976f5a \ No newline at end of file diff --git a/licenses/mips-android-sysimage-license b/licenses/mips-android-sysimage-license new file mode 100644 index 00000000..8f4f164e --- /dev/null +++ b/licenses/mips-android-sysimage-license @@ -0,0 +1,2 @@ + +e9acab5b5fbb560a72cfaecce8946896ff6aab9d \ No newline at end of file diff --git a/local.properties.example b/local.properties.example new file mode 100644 index 00000000..bcf2a894 --- /dev/null +++ b/local.properties.example @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. + +sdk.dir= diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts new file mode 100644 index 00000000..2bc918dd --- /dev/null +++ b/modules/openid-federation-common/build.gradle.kts @@ -0,0 +1,139 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + kotlin("plugin.serialization") version "2.0.0" +} + +kotlin { + @OptIn(ExperimentalWasmDsl::class) + + js { + browser { + commonWebpackConfig { + devServer = KotlinWebpackConfig.DevServer().apply { + port = 8083 + } + } + } + nodejs { + testTask { + useMocha { + timeout = "5000" + } + } + } + } + + // wasmJs is not available yet for ktor until v3.x is released which is still in alpha + + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.ktor:ktor-client-core:2.3.11") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:2.3.11") + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + + val androidMain by getting { + dependencies { + implementation("io.ktor:ktor-client-okhttp:2.3.11") + } + } + val androidUnitTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + + val iosMain by creating { + dependsOn(commonMain) + dependencies { + implementation("io.ktor:ktor-client-ios:2.3.11") + } + } + val iosX64Main by getting { + dependsOn(iosMain) + } + val iosArm64Main by getting { + dependsOn(iosMain) + } + val iosSimulatorArm64Main by getting { + dependsOn(iosMain) + } + + val iosTest by creating { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test")) + } + } + + val jsMain by getting { + dependencies { + implementation("io.ktor:ktor-client-js:2.3.11") + } + } + + val jsTest by getting { + dependsOn(commonTest) + dependencies { + implementation(kotlin("test-js")) + implementation(kotlin("test-annotations-common")) + } + } + } +} + +tasks.register("printSdkLocation") { + doLast { + println("Android SDK Location: ${android.sdkDirectory}") + } +} + +android { + namespace = "com.sphereon.oid.fed.common" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} + diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt new file mode 100644 index 00000000..2a97e8eb --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoder.kt @@ -0,0 +1,144 @@ +package com.sphereon.oid.fed.common.mime + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.js.JsExport + +private val qpAllowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~".toSet() + +/** + * URL encode a String. + * Converts characters not allowed in URL query parameters to their percent-encoded equivalents. + * input an input string + * @return URL encoded String + */ +@JsExport +fun urlEncodeValue(input: String): String { + return buildString { + input.forEach { char -> + if (char in qpAllowedChars) { + append(char) + } else { + append('%') + append(char.code.toString(16).uppercase().padStart(2, '0')) + } + } + } +} + +/** + * Extension function to URL encode a String. + * Converts characters not allowed in URL query parameters to their percent-encoded equivalents. + * + * @return URL encoded String + */ +fun String.toUrlEncodedValue(): String { + return urlEncodeValue(this) +} + +/** + * Extension function to URL encode a JsonElement. + * Converts the JsonElement to a JSON String and then URL encodes it. + * + * @return URL encoded String representation of the JsonElement + */ +fun JsonElement.toUrlEncodedValue(): String { + return this.toString().toUrlEncodedValue() +} + +/** + * Inline function to URL encode any serializable object. + * Converts the object to a JSON string and then URL encodes it. + * + * @return URL encoded JSON String representation of the object + */ +inline fun T.toUrlEncodedJsonValue(): String { + return Json.encodeToString(this).toUrlEncodedValue() +} + +/** + * Function to URL encode any serializable object using a provided serializer. + * Converts the object to a JSON string using the serializer and then URL encodes it. + * + * @param serializer The serializer to use for converting the object to a JSON string + * @return URL encoded JSON String representation of the object + */ +fun T.toUrlEncodedJsonValue(serializer: KSerializer): String { + return Json.encodeToString(serializer, this).toUrlEncodedValue() +} + +/** + * Extension function to decode a URL encoded String. + * Converts percent-encoded characters back to their original form. + * + * input An URL encoded input string + * @return Decoded String + */ +@JsExport +fun urlDecodeValue(input: String): String { + return buildString { + var i = 0 + while (i < input.length) { + when (val char = input[i]) { + '%' -> { + if (i + 2 >= input.length) { + throw IllegalArgumentException("Incomplete percent encoding at position $i") + } + append(input.substring(i + 1, i + 3).toInt(16).toChar()) + i += 3 + } + + else -> { + append(char) + i++ + } + } + } + } +} + +/** + * Extension function to decode a URL encoded String. + * Converts percent-encoded characters back to their original form. + * + * @return Decoded String + */ +fun String.fromUrlEncodedValue(): String { + return urlDecodeValue(this) +} + +/** + * Extension function to decode a URL encoded JSON String to a JsonElement. + * Decodes the URL encoded String and parses it to a JsonElement. + * + * @return Decoded JsonElement + */ +fun String.fromUrlEncodedJsonValueToJsonElement(): JsonElement { + val decodedString = this.fromUrlEncodedValue() + return Json.parseToJsonElement(decodedString) +} + +/** + * Inline function to decode a URL encoded JSON String to an object of type T. + * Decodes the URL encoded String and deserializes it to an object of type T. + * + * @return Deserialized object of type T + */ +inline fun String.fromUrlEncodedJsonValue(): T { + val decodedString = this.fromUrlEncodedValue() + return Json.decodeFromString(decodedString) +} + +/** + * Function to decode a URL encoded JSON String to an object of type T using a provided serializer. + * Decodes the URL encoded String and deserializes it to an object of type T using the serializer. + * + * @param serializer The serializer to use for deserializing the JSON string + * @return Deserialized object of type T + */ +fun String.fromUrlEncodedJsonValue(serializer: KSerializer): T { + val decodedString = this.fromUrlEncodedValue() + return Json.decodeFromString(serializer, decodedString) +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt new file mode 100644 index 00000000..f58b41ed --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderKtor.kt @@ -0,0 +1,92 @@ +package com.sphereon.oid.fed.common.mime + +import io.ktor.http.* +import kotlinx.serialization.json.JsonElement + +/** + * Extension function for ParametersBuilder to append a URL encoded value. + * Encodes the given data string and appends it to the ParametersBuilder with the specified name. + * + * @param name The name of the parameter + * @param data The data to be URL encoded and appended + * @return The ParametersBuilder with the appended value + * @throws IllegalArgumentException if the name or data is empty + */ +fun ParametersBuilder.appendUrlEncodedValue(name: String, data: String): ParametersBuilder { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + require(data.isNotEmpty()) { "data cannot be empty" } + + this.append(name, data.toUrlEncodedValue()) + return this +} + +/** + * Extension function for ParametersBuilder to append a URL encoded JsonElement. + * Converts the JsonElement to a string, URL encodes it, and appends it to the ParametersBuilder with the specified name. + * + * @param name The name of the parameter + * @param jsonElement The JsonElement to be URL encoded and appended + * @return The ParametersBuilder with the appended value + * @throws IllegalArgumentException if the name is empty + */ +fun ParametersBuilder.appendUrlEncodedValue(name: String, jsonElement: JsonElement): ParametersBuilder { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + + this.append(name, jsonElement.toString().toUrlEncodedValue()) + return this +} + +/** + * Extension function for Parameters to decode URL encoded values. + * Decodes all URL encoded values in the Parameters and returns them as a Map. + * + * @return A Map containing the decoded parameter names and values + */ +fun Parameters.fromUrlEncodedValues(): Map { + return this.entries().mapNotNull { + val value = it.value.firstOrNull()?.fromUrlEncodedValue() + if (value != null) it.key to value else null + }.toMap() +} + +/** + * Extension function for Parameters to decode URL encoded JSON values to JsonElements. + * Decodes all URL encoded JSON values in the Parameters and returns them as a Map. + * + * @return A Map containing the decoded parameter names and JsonElements + */ +fun Parameters.fromUrlEncodedJsonValuesToJsonElements(): Map { + return this.entries().mapNotNull { + val value = it.value.firstOrNull()?.fromUrlEncodedJsonValueToJsonElement() + if (value != null) it.key to value else null + }.toMap() +} + +/** + * Extension function for Parameters to get and decode a URL encoded value. + * Retrieves the value for the specified name, decodes it, and returns the result. + * + * @param name The name of the parameter to retrieve + * @return The decoded value + * @throws IllegalArgumentException if the name is empty + * @throws NoSuchElementException if no value is found for the specified name + */ +fun Parameters.getUrlEncodedValue(name: String): String { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + return this[name]?.fromUrlEncodedValue() ?: throw NoSuchElementException("No value found for key: $name") +} + +/** + * Extension function for Parameters to get and decode a URL encoded JSON value to a JsonElement. + * Retrieves the value for the specified name, decodes it to a JsonElement, and returns the result. + * + * @param name The name of the parameter to retrieve + * @return The decoded JsonElement + * @throws IllegalArgumentException if the name is empty + * @throws NoSuchElementException if no value is found for the specified name + */ +fun Parameters.getUrlEncodedJsonValueToJsonElement(name: String): JsonElement { + require(name.isNotEmpty()) { "Parameter name cannot be empty" } + return this[name]?.fromUrlEncodedJsonValueToJsonElement() + ?: throw NoSuchElementException("No value found for key: $name") +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt new file mode 100644 index 00000000..97223838 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTest.kt @@ -0,0 +1,87 @@ +package com.sphereon.oid.fed.common.mime + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test +import kotlin.test.assertEquals + + +@Serializable +data class PreAuthorizedCode(val pre_authorized_code: String) + +@Serializable +data class Grants(val urn_ietf_params_oauth_grant_type_pre_authorized_code: PreAuthorizedCode) + +@Serializable +data class TestData( + val grants: Grants, + val credential_configuration_ids: List, + val credential_issuer: String +) + +class JsonUrlEncoderTest { + private val originalJson = + """{"grants":{"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"a"}},"credential_configuration_ids":["DummyCredential"],"credential_issuer":"https://agent.issuer.dummy.com"}""" + private val encodedJson = + "%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + private val encodedJsonFromObject = // In objects, we can have colons in the field name + "%7B%22grants%22%3A%7B%22urn_ietf_params_oauth_grant_type_pre_authorized_code%22%3A%7B%22pre_authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + + private val testData = TestData( + grants = Grants(PreAuthorizedCode("a")), + credential_configuration_ids = listOf("DummyCredential"), + credential_issuer = "https://agent.issuer.dummy.com" + ) + + @Test + fun testStringToUrlEncodedValue() { + val result = originalJson.toUrlEncodedValue() + assertEquals(encodedJson, result) + } + + @Test + fun testJsonElementToUrlEncodedValue() { + val original: JsonElement = Json.parseToJsonElement(originalJson) + val result = original.toUrlEncodedValue() + assertEquals(encodedJson, result) + } + + @Test + fun testObjectToUrlEncodedJsonValue() { + val result = testData.toUrlEncodedJsonValue() + assertEquals(encodedJsonFromObject, result) + } + + @Test + fun testKSerializerToUrlEncodedJsonValue() { + val serializer = TestData.serializer() + val result = testData.toUrlEncodedJsonValue(serializer) + assertEquals(encodedJsonFromObject, result) + } + + @Test + fun testFromUrlEncodedValue() { + val result = encodedJson.fromUrlEncodedValue() + assertEquals(originalJson, result) + } + + @Test + fun testFromUrlEncodedValueToJsonElement() { + val result = encodedJson.fromUrlEncodedJsonValueToJsonElement() + assertEquals(Json.parseToJsonElement(originalJson), result) + } + + @Test + fun testFromUrlEncodedJsonValue() { + val result: TestData = encodedJsonFromObject.fromUrlEncodedJsonValue() + assertEquals(testData, result) + } + + @Test + fun testFromUrlEncodedJsonValueWithSerializer() { + val serializer = TestData.serializer() + val result: TestData = encodedJsonFromObject.fromUrlEncodedJsonValue(serializer) + assertEquals(testData, result) + } +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt new file mode 100644 index 00000000..da6cc437 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/mime/JsonUrlEncoderTestKtor.kt @@ -0,0 +1,86 @@ +package com.sphereon.oid.fed.common.mime + +import io.ktor.http.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class JsonUrlEncoderTestKtor { + private val originalJson = + """{"grants":{"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"a"}},"credential_configuration_ids":["DummyCredential"],"credential_issuer":"https://agent.issuer.dummy.com"}""" + private val encodedJson = + "%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22DummyCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fagent.issuer.dummy.com%22%7D" + + + @Test + fun testAppendUrlEncodedValueWithString() { + val parametersBuilder = ParametersBuilder() + parametersBuilder.appendUrlEncodedValue("test", originalJson) + val result = parametersBuilder.build()["test"] + assertEquals(encodedJson, result) + } + + @Test + fun testAppendUrlEncodedValueWithJsonElement() { + val jsonElement: JsonElement = Json.parseToJsonElement(originalJson) + val parametersBuilder = ParametersBuilder() + parametersBuilder.appendUrlEncodedValue("test", jsonElement) + val result = parametersBuilder.build()["test"] + assertEquals(encodedJson, result) + } + + @Test + fun testFromUrlEncodedValues() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.fromUrlEncodedValues() + assertEquals(mapOf("test" to originalJson), result) + } + + @Test + fun testFromUrlEncodedJsonValuesToJsonElements() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.fromUrlEncodedJsonValuesToJsonElements() + assertEquals(mapOf("test" to Json.parseToJsonElement(originalJson)), result) + } + + @Test + fun testGetUrlEncodedValue() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.getUrlEncodedValue("test") + assertEquals(originalJson, result) + } + + @Test + fun testGetUrlEncodedJsonValueToJsonElement() { + val parameters = ParametersBuilder().apply { + append("test", encodedJson) + }.build() + val result = parameters.getUrlEncodedJsonValueToJsonElement("test") + assertEquals(Json.parseToJsonElement(originalJson), result) + } + + @Test + fun testGetUrlEncodedValueThrowsException() { + val parameters = ParametersBuilder().build() + assertFailsWith { + parameters.getUrlEncodedValue("test") + } + } + + @Test + fun testGetUrlEncodedJsonValueToJsonElementThrowsException() { + val parameters = ParametersBuilder().build() + assertFailsWith { + parameters.getUrlEncodedJsonValueToJsonElement("test") + } + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..fbf00ad3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,31 @@ +rootProject.name = "kotlin-mp-genesis" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +include(":modules:openid-federation-common")