diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8709cb6 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +Please refer to the [OpenFeature community page](https://openfeature.dev/community/#code-of-conduct) for more information on Code of Conduct. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1f4bca0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: + - 'main' + pull_request: + branches: + - '*' + +jobs: + Tests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run checks + run: ./gradlew check --no-daemon --stacktrace diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 0000000..2a3907e --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,42 @@ +name: 'Lint PR' + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: marocchino/sticky-pull-request-comment@v2 + # When the previous steps fails, the workflow would stop. By adding this + # condition you can continue the execution with the populated error message. + if: always() && (steps.lint_pr_title.outputs.error_message != null) + with: + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! 👋🏼 + + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. + + Details: + + ``` + ${{ steps.lint_pr_title.outputs.error_message }} + ``` + + # Delete a previous comment when the issue has been resolved + - if: ${{ steps.lint_pr_title.outputs.error_message == null }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + delete: true diff --git a/.gitignore b/.gitignore index 566e06b..cf11e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,14 @@ -# Compiled class file -*.class +# Ignore Gradle project-specific cache directory +.gradle -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects +.kotlin/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +# Ignore Gradle build output directory +build -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +# Ignore IntelliJ files +.idea -# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects -.kotlin/ \ No newline at end of file +# Ignore Kotlin files +.kotlin \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e28ebd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +TODO \ No newline at end of file diff --git a/OWNERS.md b/OWNERS.md new file mode 100644 index 0000000..3f7f67c --- /dev/null +++ b/OWNERS.md @@ -0,0 +1,13 @@ +# Owners + +- See [CONTRIBUTING.md](CONTRIBUTING.md) for general contribution guidelines. + +## Core Developers + +- Fabrizio Demaria (fabriziodemaria, Spotify) +- Nicklas Lundin (nicklasl, Spotify) +- Nicky Bondarenko (nickybondarenko, Spotify) +- Todd Baert (toddbaert, Dynatrace) +- Vahid Torkaman (vahidlazio, Spotify) + +See https://github.com/open-feature/community/blob/main/config/open-feature/sdk-kotlin/workgroup.yaml. \ No newline at end of file diff --git a/README.md b/README.md index 4423e15..add0962 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ -# kotlin-sdk-contrib \ No newline at end of file +# OpenFeature Kotlin Contributions + +> [!WARNING] +> This repository is not fully initialized for the moment, proceed at your own risk. + +This repository is intended for OpenFeature contributions which are not included in the [OpenFeature Kotlin SDK](https://github.com/open-feature/kotlin-sdk). + +The project includes: + +- [Providers](./providers) +- [Hooks](./hooks) + +### Software Bill of Materials (SBOM) + +We publish SBOMs with all of our releases. You can find them in Maven Central alongside the artifacts. + +## Contributing + +see: [CONTRIBUTING.md](./CONTRIBUTING.md) + +## License + +Apache 2.0 - See [LICENSE](./LICENSE) for more information. + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..722dc5b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.nexus.publish) +} + +allprojects { + extra["groupId"] = "dev.openfeature.kotlin.contrib" + ext["version"] = "0.1.0" +} +group = project.extra["groupId"].toString() +version = project.extra["version"].toString() + +nexusPublishing { + this.repositories { + sonatype { + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set( + uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"), + ) + username = System.getenv("OSSRH_USERNAME") + password = System.getenv("OSSRH_PASSWORD") + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..00b5138 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.configuration-cache=true + +# This seems to be necessary for Coroutines to work on JS. Otherwise getting the following error: +# > Task :providers:env-var:compileTestDevelopmentExecutableKotlinJs FAILED +# e: java.lang.IllegalStateException: IC internal error: can not find library org.jetbrains.kotlin:kotlinx-atomicfu-runtime +kotlinx.atomicfu.enableJsIrTransformation=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..68dbc80 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +kotlin = "2.1.21" +kotlinx-coroutines = "1.10.2" +open-feature-kotlin-sdk = "0.4.1" + +[libraries] +openfeature-kotlin-sdk = { group="dev.openfeature", name="kotlin-sdk", version.ref="open-feature-kotlin-sdk" } +kotlin-test = { group="org.jetbrains.kotlin", name="kotlin-test", version.ref="kotlin" } +kotlinx-coroutines-core = { group="org.jetbrains.kotlinx", name="kotlinx-coroutines-core", version.ref="kotlinx-coroutines" } +kotlinx-coroutines-test = { group="org.jetbrains.kotlinx", name="kotlinx-coroutines-test", version.ref="kotlinx-coroutines" } + +[plugins] +kotlin-multiplatform = { id="org.jetbrains.kotlin.multiplatform", version.ref="kotlin" } +kotlinx-atomicfu = { id="org.jetbrains.kotlinx.atomicfu", version="0.27.0" } +ktlint = { id="org.jlleitschuh.gradle.ktlint", version="12.3.0" } +nexus-publish = { id="io.github.gradle-nexus.publish-plugin", version="2.0.0" } +binary-compatibility-validator = { id="org.jetbrains.kotlinx.binary-compatibility-validator", version="0.17.0" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9bbc975 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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, 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 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..73cea24 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,3 @@ +# OpenFeature Kotlin Hooks + +Hooks are a mechanism whereby application developers can add arbitrary behavior to flag evaluation. They operate similarly to middleware in many web frameworks. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md) for more details. diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 0000000..f4373ae --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,519 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +debug@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +kotlin-web-helpers@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.0.0.tgz#b112096b273c1e733e0b86560998235c09a19286" + integrity sha512-xkVGl60Ygn/zuLkDPx+oHj7jeLR7hCvoNF99nhwXMn8a3ApB4lLiC9pk4ol4NHPjyoCbvQctBqvzUcp8pkqyWw== + dependencies: + format-util "^1.0.5" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +mocha@10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" + integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +source-map-support@0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..e3a6084 --- /dev/null +++ b/local.properties @@ -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. +#Sun May 18 19:42:09 CEST 2025 +sdk.dir=/home/bence/Android/Sdk diff --git a/providers/README.md b/providers/README.md new file mode 100644 index 0000000..c288b8c --- /dev/null +++ b/providers/README.md @@ -0,0 +1,3 @@ +# OpenFeature Kotlin Providers + +Providers are responsible for performing flag evaluation. They provide an abstraction between the underlying flag management system and OpenFeature itself. This allows providers to be changed without requiring a major code refactor. Please see the [spec](https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md) for more details. diff --git a/providers/env-var/README.md b/providers/env-var/README.md new file mode 100644 index 0000000..ddae9ea --- /dev/null +++ b/providers/env-var/README.md @@ -0,0 +1,93 @@ +# Environment Variables Kotlin Provider + +Environment Variables provider allows you to read feature flags from the [process's environment](https://en.wikipedia.org/wiki/Environment_variable). + +## Supported platforms + +| Supported | Platform | Supported versions | +|-----------|----------------------|--------------------------------------------------------------------------------| +| ❌ | Android | | +| ✅ | JVM | JDK 11+ | +| ✅ | Native | Linux x64 | +| ❌ | Native | [Other native targets](https://kotlinlang.org/docs/native-target-support.html) | +| ✅ | Javascript (Node.js) | | +| ❌ | Javascript (Browser) | | +| ❌ | Wasm | | + + +## Installation + +```xml + + dev.openfeature.kotlin.contrib.providers + env-var + 0.1.0 + +``` + +## Usage + +To use the `EnvVarProvider` create an instance and use it as a provider: + +```kotlin + val provider = EnvVarProvider() + OpenFeatureAPI.setProviderAndWait(provider) +``` + +### Configuring different methods for fetching environment variables + +This provider defines an `EnvironmentGateway` interface, which is used to access the actual environment variables. +The method [`platformSpecificEnvironmentGateway`][platformSpecificEnvironmentGateway], which is implemented for each supported platform, returns a default implementation. + +```kotlin + val testFake = EnvironmentGateway { arg -> "true" } // always returns true + + val provider = EnvVarProvider(testFake) + OpenFeatureAPI.getInstance().setProvider(provider) +``` + +### Key transformation + +This provider supports transformation of keys to support different patterns used for naming feature flags and for +naming environment variables, e.g. SCREAMING_SNAKE_CASE env variables vs. hyphen-case keys for feature flags. +It supports chaining/combining different transformers incl. self-written ones by providing a transforming function in the constructor. +Currently, the following transformations are supported out of the box: + +- converting to lower case (e.g. `Feature.Flag` => `feature.flag`) +- converting to UPPER CASE (e.g. `Feature.Flag` => `FEATURE.FLAG`) +- converting hyphen-case to SCREAMING_SNAKE_CASE (e.g. `Feature-Flag` => `FEATURE_FLAG`) +- convert to camelCase (e.g. `FEATURE_FLAG` => `featureFlag`) +- replace '_' with '.' (e.g. `feature_flag` => `feature.flag`) +- replace '.' with '_' (e.g. `feature.flag` => `feature_flag`) + +**Examples:** + +1. hyphen-case feature flag names to screaming snake-case environment variables: + + ```kotlin + // Definition of the EnvVarProvider: + val provider = EnvVarProvider(EnvironmentKeyTransformer.hyphenCaseToScreamingSnake()) + ``` + +2. chained/composed transformations: + + ```kotlin + // Definition of the EnvVarProvider: + val keyTransformer = EnvironmentKeyTransformer + .toLowerCaseTransformer() + .andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer()) + + val provider = EnvVarProvider(keyTransformer) + ``` + +3. freely defined transformation function: + + ```kotlin + // Definition of the EnvVarProvider: + val keyTransformer = EnvironmentKeyTransformer { key -> key.substring(1) } + val provider = EnvVarProvider(keyTransformer) + ``` + + + +[platformSpecificEnvironmentGateway]: src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.kt diff --git a/providers/env-var/api/env-var.api b/providers/env-var/api/env-var.api new file mode 100644 index 0000000..20660a4 --- /dev/null +++ b/providers/env-var/api/env-var.api @@ -0,0 +1,50 @@ +public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider : dev/openfeature/sdk/FeatureProvider { + public static final field Companion Ldev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider$Companion; + public fun ()V + public fun (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway;Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;)V + public synthetic fun (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway;Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun getBooleanEvaluation (Ljava/lang/String;ZLdev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation; + public fun getDoubleEvaluation (Ljava/lang/String;DLdev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation; + public fun getHooks ()Ljava/util/List; + public fun getIntegerEvaluation (Ljava/lang/String;ILdev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation; + public fun getMetadata ()Ldev/openfeature/sdk/ProviderMetadata; + public fun getObjectEvaluation (Ljava/lang/String;Ldev/openfeature/sdk/Value;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation; + public fun getStringEvaluation (Ljava/lang/String;Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;)Ldev/openfeature/sdk/ProviderEvaluation; + public fun initialize (Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun observe ()Lkotlinx/coroutines/flow/Flow; + public fun onContextSet (Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/EvaluationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun shutdown ()V + public fun track (Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/TrackingEventDetails;)V +} + +public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider$Companion { +} + +public abstract interface class dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway { + public abstract fun getEnvironmentVariable (Ljava/lang/String;)Ljava/lang/String; +} + +public abstract interface class dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer { + public static final field Companion Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer$Companion; + public abstract fun andThen (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;)Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public abstract fun transformKey (Ljava/lang/String;)Ljava/lang/String; +} + +public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer$Companion { + public final fun doNothing ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun hyphenCaseToScreamingSnake ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun replaceDotWithUnderscoreTransformer ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun replaceUnderscoreWithDotTransformer ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun toCamelCaseTransformer ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun toLowerCaseTransformer ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; + public final fun toUpperCaseTransformer ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; +} + +public final class dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer$DefaultImpls { + public static fun andThen (Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer;)Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer; +} + +public final class dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway_jvmKt { + public static final fun platformSpecificEnvironmentGateway ()Ldev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway; +} + diff --git a/providers/env-var/build.gradle.kts b/providers/env-var/build.gradle.kts new file mode 100644 index 0000000..3ce8e16 --- /dev/null +++ b/providers/env-var/build.gradle.kts @@ -0,0 +1,51 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.binary.compatibility.validator) + alias(libs.plugins.ktlint) + // Needed for the JS coroutine support for the tests + alias(libs.plugins.kotlinx.atomicfu) +} + +kotlin { + jvm { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + } + } + linuxX64 {} + js { + nodejs() + } + + sourceSets { + commonMain.dependencies { + api(libs.openfeature.kotlin.sdk) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.test) + } + } +} + +// Set test environment variable for tests +// Used in ./commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGatewayTest.kt +val testEnvironmentVariable = "TEST_ENVIRONMENT_VARIABLE" to "foo" +tasks.withType(Test::class).configureEach { + environment(testEnvironmentVariable) +} +tasks.withType(KotlinJsTest::class).configureEach { + environment(testEnvironmentVariable.first, testEnvironmentVariable.second) +} +tasks.withType(KotlinNativeTest::class).configureEach { + environment(testEnvironmentVariable.first, testEnvironmentVariable.second) +} diff --git a/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/CamelCase.kt b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/CamelCase.kt new file mode 100644 index 0000000..dff943c --- /dev/null +++ b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/CamelCase.kt @@ -0,0 +1,26 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +/** + * Converts an underscore-separated string to camel case. + */ +internal fun String.camelCase(): String { + // Split by underscores + val words = + split('_') + .filter { it.isNotEmpty() } // Remove empty strings that might result from splitting + + if (words.isEmpty()) { + return "" + } + + // The first word is converted to lowercase + val firstWord = words.first().lowercase() + + // Subsequent words are capitalized (first letter uppercase, rest lowercase) + val restOfWords = + words.drop(1).joinToString("") { word -> + word.lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + } + + return firstWord + restOfWords +} diff --git a/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider.kt b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider.kt new file mode 100644 index 0000000..371a392 --- /dev/null +++ b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProvider.kt @@ -0,0 +1,92 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import dev.openfeature.sdk.EvaluationContext +import dev.openfeature.sdk.FeatureProvider +import dev.openfeature.sdk.Hook +import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.ProviderMetadata +import dev.openfeature.sdk.Reason +import dev.openfeature.sdk.Value +import dev.openfeature.sdk.exceptions.OpenFeatureError + +/** EnvVarProvider is the Kotlin provider implementation for the environment variables. */ +class EnvVarProvider( + private val environmentGateway: EnvironmentGateway = platformSpecificEnvironmentGateway(), + private val keyTransformer: EnvironmentKeyTransformer = EnvironmentKeyTransformer.doNothing(), +) : FeatureProvider { + override val hooks: List> = emptyList() + override val metadata: ProviderMetadata + get() = Metadata + + override suspend fun initialize(initialContext: EvaluationContext?) { + // Nothing to do here + } + + override fun shutdown() { + // Nothing to do here + } + + override suspend fun onContextSet( + oldContext: EvaluationContext?, + newContext: EvaluationContext, + ) { + // Nothing to do here + } + + override fun getBooleanEvaluation( + key: String, + defaultValue: Boolean, + context: EvaluationContext?, + ): ProviderEvaluation = evaluateEnvironmentVariable(key, String::toBoolean) + + override fun getDoubleEvaluation( + key: String, + defaultValue: Double, + context: EvaluationContext?, + ): ProviderEvaluation = evaluateEnvironmentVariable(key, String::toDouble) + + override fun getIntegerEvaluation( + key: String, + defaultValue: Int, + context: EvaluationContext?, + ): ProviderEvaluation = evaluateEnvironmentVariable(key, String::toInt) + + override fun getStringEvaluation( + key: String, + defaultValue: String, + context: EvaluationContext?, + ): ProviderEvaluation = evaluateEnvironmentVariable(key, { it }) + + override fun getObjectEvaluation( + key: String, + defaultValue: Value, + context: EvaluationContext?, + ): ProviderEvaluation = throw OpenFeatureError.GeneralError("EnvVarProvider supports only primitives") + + private fun evaluateEnvironmentVariable( + key: String, + parse: (String) -> T, + ): ProviderEvaluation { + val value: String = + environmentGateway.getEnvironmentVariable(keyTransformer.transformKey(key)) + ?: throw OpenFeatureError.FlagNotFoundError(key) + + try { + return ProviderEvaluation( + value = parse(value), + reason = Reason.STATIC.toString(), + ) + } catch (e: Exception) { + throw OpenFeatureError.ParseError(e.message ?: "Unknown parsing error") + } + } + + companion object { + private val NAME: String = "Environment Variables Provider" + } + + private object Metadata : ProviderMetadata { + override val name: String + get() = NAME + } +} diff --git a/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway.kt b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway.kt new file mode 100644 index 0000000..e2b233e --- /dev/null +++ b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentGateway.kt @@ -0,0 +1,11 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +/** + * This is an abstraction to fetch environment variables. It can be used to support + * environment-specific access or provide additional functionality, like prefixes, casing and even + * sources like spring configurations which come from different sources. Also, a test double could + * implement this interface, making the tests independent of the actual environment. + */ +fun interface EnvironmentGateway { + fun getEnvironmentVariable(key: String): String? +} diff --git a/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer.kt b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer.kt new file mode 100644 index 0000000..a358f6f --- /dev/null +++ b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformer.kt @@ -0,0 +1,81 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +/** + * This class provides a way to transform any given key to another value. This is helpful, if keys in the code have a + * different representation as in the actual environment, e.g. SCREAMING_SNAKE_CASE env vars vs. hyphen-case keys + * for feature flags. + * + * + * This class also supports chaining/combining different transformers incl. self-written ones by providing + * a transforming function in the constructor.

+ * Currently, the following transformations are supported out of the box: + * + * * [converting to lower case][.toLowerCaseTransformer] + * * [converting to UPPER CASE][.toUpperCaseTransformer] + * * [converting hyphen-case to SCREAMING_SNAKE_CASE][.hyphenCaseToScreamingSnake] + * * [convert to camelCase][.toCamelCaseTransformer] + * * [replace '_' with '.'][.replaceUnderscoreWithDotTransformer] + * * [replace '.' with '_'][.replaceDotWithUnderscoreTransformer] + * + * + * + * **Examples:** + * + * + * 1. hyphen-case feature flag names to screaming snake-case environment variables: + *
+ * `// Definition of the EnvVarProvider:
+ * EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
+ * .hyphenCaseToScreamingSnake();
+ *
+ * FeatureProvider provider = new EnvVarProvider(transformer);
+` *
+
* + * 2. chained/composed transformations: + *
+ * `// Definition of the EnvVarProvider:
+ * EnvironmentKeyTransformer transformer = EnvironmentKeyTransformer
+ * .toLowerCaseTransformer()
+ * .andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer());
+ *
+ * FeatureProvider provider = new EnvVarProvider(transformer);
+` *
+
* + * 3. freely defined transformation function: + *
+ * `// Definition of the EnvVarProvider:
+ * EnvironmentKeyTransformer transformer = new EnvironmentKeyTransformer(key -> "constant");
+ *
+ * FeatureProvider provider = new EnvVarProvider(keyTransformer);
+` *
+
* + */ +fun interface EnvironmentKeyTransformer { + fun transformKey(key: String): String + + fun andThen(another: EnvironmentKeyTransformer): EnvironmentKeyTransformer = + EnvironmentKeyTransformer { key -> + another.transformKey( + this.transformKey(key), + ) + } + + companion object { + fun toLowerCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.lowercase() } + + fun toUpperCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.uppercase() } + + fun toCamelCaseTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.camelCase() } + + fun replaceUnderscoreWithDotTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.replace('_', '.') } + + fun replaceDotWithUnderscoreTransformer(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { key -> key.replace('.', '_') } + + fun hyphenCaseToScreamingSnake(): EnvironmentKeyTransformer = + EnvironmentKeyTransformer { key -> + key.replace('-', '_').uppercase() + } + + fun doNothing(): EnvironmentKeyTransformer = EnvironmentKeyTransformer { s -> s } + } +} diff --git a/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.kt b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.kt new file mode 100644 index 0000000..b3dc56b --- /dev/null +++ b/providers/env-var/src/commonMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.kt @@ -0,0 +1,3 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +internal expect fun platformSpecificEnvironmentGateway(): EnvironmentGateway diff --git a/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderE2eTest.kt b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderE2eTest.kt new file mode 100644 index 0000000..577a22f --- /dev/null +++ b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderE2eTest.kt @@ -0,0 +1,55 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import dev.openfeature.sdk.Client +import dev.openfeature.sdk.OpenFeatureAPI +import dev.openfeature.sdk.Reason +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +// The TEST_ENVIRONMENT_VARIABLE environment variable is defined in `build.gradle.kts` +private const val EXISTING_FEATURE_FLAG_NAME = "test.environment.variable" +private const val EXISTING_FEATURE_FLAG_VALUE = "foo" +private const val NON_EXISTING_FEATURE_FLAG_NAME = "non.existing.test.environment.variable" +private const val DEFAULT_VALUE = "default value" + +class EnvVarProviderE2eTest { + // Converts `test.environment.variable` to `TEST_ENVIRONMENT_VARIABLE` + private val environmentKeyTransformer = + EnvironmentKeyTransformer + .toUpperCaseTransformer() + .andThen(EnvironmentKeyTransformer.replaceDotWithUnderscoreTransformer()) + + private val provider = + EnvVarProvider( + platformSpecificEnvironmentGateway(), + environmentKeyTransformer, + ) + + @Test + fun `should return feature flag value if present`() = + withClient { client -> + val result = client.getStringDetails(EXISTING_FEATURE_FLAG_NAME, DEFAULT_VALUE) + assertEquals(EXISTING_FEATURE_FLAG_VALUE, result.value) + assertEquals(Reason.STATIC.toString(), result.reason) + } + + @Test + fun `should return default value if feature flag is not present`() = + withClient { client -> + val result = client.getStringDetails(NON_EXISTING_FEATURE_FLAG_NAME, DEFAULT_VALUE) + assertEquals(DEFAULT_VALUE, result.value) + assertEquals(Reason.ERROR.toString(), result.reason) + } + + private fun withClient(callback: (client: Client) -> Unit) = + runTest { + try { + OpenFeatureAPI.setProviderAndWait(provider) + val client = OpenFeatureAPI.getClient() + callback(client) + } finally { + OpenFeatureAPI.shutdown() + } + } +} diff --git a/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderTest.kt b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderTest.kt new file mode 100644 index 0000000..4f9dddb --- /dev/null +++ b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvVarProviderTest.kt @@ -0,0 +1,302 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import dev.openfeature.sdk.FeatureProvider +import dev.openfeature.sdk.ImmutableContext +import dev.openfeature.sdk.ProviderEvaluation +import dev.openfeature.sdk.Reason +import dev.openfeature.sdk.Value +import dev.openfeature.sdk.exceptions.OpenFeatureError +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +private val REASON_STATIC = Reason.STATIC.toString() + +internal class EnvVarProviderTest { + @Test + fun `should throw on getObjectEvaluation`() { + assertFailsWith { + EnvVarProvider() + .getObjectEvaluation("any-key", Value.Null, ImmutableContext()) + } + } + + @Test + fun `should evaluate true as Boolean correctly`() = + evaluationTest( + "bool_true", + "true", + { + getBooleanEvaluation( + "bool_true", + false, + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + true, + ) + }, + ) + + @Test + fun `should evaluate false as Boolean correctly`() = + evaluationTest( + "bool_false", + "FaLsE", + { + getBooleanEvaluation( + "bool_false", + true, + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + false, + ) + }, + ) + + @Test + fun `should evaluate unrecognized Boolean as Boolean false correctly`() = + evaluationTest( + "bool_false", + "not-a-bool", + { + getBooleanEvaluation( + "bool_false", + true, + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + false, + ) + }, + ) + + @Test + fun `should evaluate String value correctly`() = + evaluationTest( + "string", + "value", + { + getStringEvaluation( + "string", + "", + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + "value", + ) + }, + ) + + @Test + fun `should evaluate Int value correctly`() = + evaluationTest( + "INT", + "42", + { + getIntegerEvaluation( + "INT", + 0, + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + 42, + ) + }, + ) + + @Test + fun `should evaluate Double value correctly`() = + evaluationTest( + "double", + "42.0", + { + getDoubleEvaluation( + "double", + 0.0, + null, + ) + }, + { evaluation -> + evaluationChecks( + evaluation, + REASON_STATIC, + 42.0, + ) + }, + ) + + @Test + fun `should throw FlagNotFound on missing Boolean env`() = + throwingEvaluationTest( + "other", + "other", + ) { + getBooleanEvaluation( + "bool_default", + true, + null, + ) + } + + @Test + fun `should throw FlagNotFound on missing String env`() = + throwingEvaluationTest( + "other", + "other", + ) { + getStringEvaluation( + "string_default", + "value", + null, + ) + } + + @Test + fun `should throw FlagNotFound on missing Int env`() = + throwingEvaluationTest( + "other", + "other", + ) { + getIntegerEvaluation( + "int_default", + 42, + null, + ) + } + + @Test + fun `should throw FlagNotFound on missing Double env`() = + throwingEvaluationTest( + "other", + "other", + ) { + getDoubleEvaluation( + "double_default", + 42.0, + null, + ) + } + + @Test + fun `should throw on unparseable values`() = + throwingEvaluationTest( + "int_incorrect", + "fourty-two", + ) { + getIntegerEvaluation( + "int_incorrect", + 0, + null, + ) + } + + @Test + fun `should throw FlagNotFound on missing double_incorrect env`() = + throwingEvaluationTest( + "double_incorrect", + "fourty-two", + ) { + getDoubleEvaluation( + "double_incorrect", + 0.0, + null, + ) + } + + @Test + fun `should transform key if configured`() { + val key = "key.transformed" + val expected = "key_transformed" + + val transformer = EnvironmentKeyTransformer.replaceDotWithUnderscoreTransformer() + val gateway = EnvironmentGateway { s -> s } + val provider = EnvVarProvider(gateway, transformer) + + val environmentVariableValue = + provider.getStringEvaluation(key, "failed", null).value + + assertEquals(expected, environmentVariableValue) + } + + @Test + fun `should use passed-in EnvironmentGateway`() { + val testFake = EnvironmentGateway { s -> "true" } + + val provider = EnvVarProvider(testFake) + + val actual: Boolean = provider.getBooleanEvaluation("any key", false, null).value + + assertTrue(actual) + } + + private fun evaluationTest( + variableName: String, + value: String, + callback: FeatureProvider.() -> ProviderEvaluation, + checks: (ProviderEvaluation) -> Unit, + ) { + // Given + val provider = provider(variableName, value) + + // When + val evaluation = callback(provider) + + // Then + checks(evaluation) + } + + private inline fun throwingEvaluationTest( + variableName: String?, + value: String, + callback: FeatureProvider.() -> ProviderEvaluation, + ) { + // Given + val provider: FeatureProvider = provider(variableName, value) + + // Then + assertFailsWith { + // When + callback(provider) + } + } + + private fun provider( + variableName: String?, + value: String, + ): FeatureProvider = + EnvVarProvider( + environmentGateway = { name -> if (name == variableName) value else null }, + ) + + private fun evaluationChecks( + evaluation: ProviderEvaluation, + reason: String?, + expected: T?, + ) { + assertEquals(reason, evaluation.reason) + assertEquals(expected, evaluation.value) + } +} diff --git a/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformerTest.kt b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformerTest.kt new file mode 100644 index 0000000..db67445 --- /dev/null +++ b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/EnvironmentKeyTransformerTest.kt @@ -0,0 +1,154 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class EnvironmentKeyTransformerTest { + @Test + fun `should transform keys to lower case prior delegating call to actual gateway`() { + listOf( + "" to "", + "a" to "a", + "A" to "a", + "ABC_DEF_GHI" to "abc_def_ghi", + "ABC.DEF.GHI" to "abc.def.ghi", + "aBc" to "abc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + + val actual = EnvironmentKeyTransformer.toLowerCaseTransformer().transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should transform keys to upper case prior delegating call to actual gateway`() { + listOf( + "" to "", + "a" to "A", + "A" to "A", + "ABC_DEF_GHI" to "ABC_DEF_GHI", + "ABC.DEF.GHI" to "ABC.DEF.GHI", + "aBc" to "ABC", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + + val actual = EnvironmentKeyTransformer.toUpperCaseTransformer().transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should not transform key when using doNothing transformer`() { + listOf( + "" to "", + "a" to "a", + "A" to "A", + "ABC_DEF_GHI" to "ABC_DEF_GHI", + "ABC.DEF.GHI" to "ABC.DEF.GHI", + "aBc" to "aBc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val actual = EnvironmentKeyTransformer.doNothing().transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should transform keys to camel case prior delegating call to actual gateway`() { + listOf( + "" to "", + "a" to "a", + "A" to "a", + "ABC_DEF_GHI" to "abcDefGhi", + "ABC.DEF.GHI" to "abc.def.ghi", + "aBc" to "abc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val transformingGateway = + EnvironmentKeyTransformer.toCamelCaseTransformer() + val actual = transformingGateway.transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should transform keys according to given transformation prior delegating call to actual gateway`() { + listOf( + "" to "_", + "a" to "_a", + "A" to "_A", + "ABC_DEF_GHI" to "_ABC_DEF_GHI", + "ABC.DEF.GHI" to "_ABC.DEF.GHI", + "aBc_abc" to "_aBc_abc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val transformingGateway = + EnvironmentKeyTransformer { "_$it" } + + val actual = transformingGateway.transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should compose transformers`() { + listOf( + "" to "", + "ABC_DEF_GHI" to "abc.def.ghi", + "ABC.DEF.GHI" to "abc.def.ghi", + "aBc" to "abc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val transformingGateway = + EnvironmentKeyTransformer + .toLowerCaseTransformer() + .andThen(EnvironmentKeyTransformer.replaceUnderscoreWithDotTransformer()) + + val actual = transformingGateway.transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should support screaming snake to hyphen case keys`() { + listOf( + "" to "", + "abc-def-ghi" to "ABC_DEF_GHI", + "abc.def.ghi" to "ABC.DEF.GHI", + "abc" to "ABC", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val transformingGateway = + EnvironmentKeyTransformer.hyphenCaseToScreamingSnake() + + val actual = transformingGateway.transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } + + @Test + fun `should support replacing dot with underscore`() { + listOf( + "" to "", + "abc-def-ghi" to "abc-def-ghi", + "abc.def.ghi" to "abc_def_ghi", + "abc" to "abc", + ).forEach { testCase -> + val (originalKey, expectedTransformedKey) = testCase + val transformingGateway = + EnvironmentKeyTransformer.replaceDotWithUnderscoreTransformer() + + val actual = transformingGateway.transformKey(originalKey) + + assertEquals(expectedTransformedKey, actual) + } + } +} diff --git a/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGatewayTest.kt b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGatewayTest.kt new file mode 100644 index 0000000..421354f --- /dev/null +++ b/providers/env-var/src/commonTest/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGatewayTest.kt @@ -0,0 +1,26 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +// The TEST_ENVIRONMENT_VARIABLE environment variable is defined in `build.gradle.kts` +private const val EXISTING_ENVIRONMENT_VARIABLE_NAME = "TEST_ENVIRONMENT_VARIABLE" +private const val EXISTING_ENVIRONMENT_VARIABLE_VALUE = "foo" +private const val NON_EXISTING_ENVIRONMENT_VARIABLE_NAME = "NON_EXISTING_ENVIRONMENT_VARIABLE" + +class PlatformSpecificEnvironmentGatewayTest { + @Test + fun `should return existing environment variable`() { + val gateway = platformSpecificEnvironmentGateway() + val result = gateway.getEnvironmentVariable(EXISTING_ENVIRONMENT_VARIABLE_NAME) + assertEquals(EXISTING_ENVIRONMENT_VARIABLE_VALUE, result) + } + + @Test + fun `should return null for non-existing environment variable`() { + val gateway = platformSpecificEnvironmentGateway() + val result = gateway.getEnvironmentVariable(NON_EXISTING_ENVIRONMENT_VARIABLE_NAME) + assertNull(result) + } +} diff --git a/providers/env-var/src/jsMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.js.kt b/providers/env-var/src/jsMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.js.kt new file mode 100644 index 0000000..00dcf26 --- /dev/null +++ b/providers/env-var/src/jsMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.js.kt @@ -0,0 +1,10 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +@JsModule("process") +@JsNonModule +external val process: dynamic + +actual fun platformSpecificEnvironmentGateway(): EnvironmentGateway = + EnvironmentGateway { key -> + process.env[key]?.toString() + } diff --git a/providers/env-var/src/jvmMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.jvm.kt b/providers/env-var/src/jvmMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.jvm.kt new file mode 100644 index 0000000..81ead22 --- /dev/null +++ b/providers/env-var/src/jvmMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.jvm.kt @@ -0,0 +1,6 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +actual fun platformSpecificEnvironmentGateway(): EnvironmentGateway = + EnvironmentGateway { key -> + System.getenv(key) + } diff --git a/providers/env-var/src/nativeMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.native.kt b/providers/env-var/src/nativeMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.native.kt new file mode 100644 index 0000000..f328d58 --- /dev/null +++ b/providers/env-var/src/nativeMain/kotlin/dev/openfeature/kotlin/contrib/providers/envvar/PlatformSpecificEnvironmentGateway.native.kt @@ -0,0 +1,11 @@ +package dev.openfeature.kotlin.contrib.providers.envvar + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.getenv + +actual fun platformSpecificEnvironmentGateway(): EnvironmentGateway = + @OptIn(ExperimentalForeignApi::class) + EnvironmentGateway { key -> + getenv(key)?.toKString() + } diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f9c2c32 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c11ce6f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +rootProject.name = "open-feature-kotlin-sdk-contrib" + +dependencyResolutionManagement { + repositories { + mavenLocal() + mavenCentral() + } +} + +include(":providers:env-var")