diff --git a/.github.amrom.workers.devponent_owners.yml b/.github.amrom.workers.devponent_owners.yml
index cae64618..4fdbf288 100644
--- a/.github.amrom.workers.devponent_owners.yml
+++ b/.github.amrom.workers.devponent_owners.yml
@@ -22,6 +22,9 @@ components:
src/OpenFeature.Contrib.Providers.Statsig:
- jenshenneberg
- lattenborough
+ src/OpenFeature.Contrib.Providers.Flipt:
+ - jeandreidc
+ - markphelps
# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
@@ -45,6 +48,9 @@ components:
test/src/OpenFeature.Contrib.Providers.Statsig.Test:
- jenshenneberg
- lattenborough
+ test/src/OpenFeature.Contrib.Providers.Flipt.Test:
+ - jeandreidc
+ - markphelps
ignored-authors:
- renovate-bot
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ae9c5e92..fdfae1f6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,82 +2,74 @@ name: CI
on:
push:
- branches: [ main ]
+ branches: [main]
paths-ignore:
- - '**.md'
+ - "**.md"
pull_request:
- branches: [ main ]
+ branches: [main]
paths-ignore:
- - '**.md'
+ - "**.md"
jobs:
build:
strategy:
matrix:
- os: [ ubuntu-latest, windows-latest ]
+ os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+ pull-requests: write
+ packages: read
+
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- dotnet-version: |
- 6.0.x
- 7.0.x
- 8.0.x
- source-url: https://nuget.pkg.github.com/open-feature/index.json
-
- - name: Restore
- run: dotnet restore
-
- - name: Build
- run: dotnet build --no-restore
-
- - name: Test
- run: dotnet test --no-build --logger GitHubActions
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ global-json-file: global.json
+ source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore
+
+ - name: Test
+ run: dotnet test --no-build --logger GitHubActions
e2e:
runs-on: ubuntu-latest
- services:
- # flagd-testbed for flagd RPC provider e2e tests
- flagd:
- image: ghcr.io/open-feature/flagd-testbed:v0.5.6
- ports:
- - 8013:8013
- # sync-testbed for flagd in-process provider e2e tests
- sync:
- image: ghcr.io/open-feature/sync-testbed:v0.5.6
- ports:
- - 9090:9090
+ permissions:
+ contents: read
+ pull-requests: write
+ packages: read
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- dotnet-version: |
- 6.0.x
- 7.0.x
- 8.0.x
- source-url: https://nuget.pkg.github.com/open-feature/index.json
-
- - name: Test
- run: dotnet build && E2E=true dotnet test --logger GitHubActions
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ global-json-file: global.json
+ source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Test
+ run: dotnet build && E2E=true dotnet test --logger GitHubActions
packaging:
needs: build
@@ -89,41 +81,38 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- submodules: recursive
-
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- dotnet-version: |
- 6.0.x
- 7.0.x
- 8.0.x
- source-url: https://nuget.pkg.github.com/open-feature/index.json
-
- - name: Restore
- run: dotnet restore
-
- - name: Pack NuGet packages (CI versions)
- if: startsWith(github.ref, 'refs/heads/')
- run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
-
- - name: Pack NuGet packages (PR versions)
- if: startsWith(github.ref, 'refs/pull/')
- run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
-
- - name: Publish NuGet packages (base)
- if: github.event.pull_request.head.repo.fork == false
- run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json
-
- - name: Publish NuGet packages (fork)
- if: github.event.pull_request.head.repo.fork == true
- uses: actions/upload-artifact@v4.3.6
- with:
- name: nupkgs
- path: src/**/*.nupkg
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ global-json-file: global.json
+ source-url: https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Pack NuGet packages (CI versions)
+ if: startsWith(github.ref, 'refs/heads/')
+ run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
+
+ - name: Pack NuGet packages (PR versions)
+ if: startsWith(github.ref, 'refs/pull/')
+ run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}"
+
+ - name: Publish NuGet packages (base)
+ if: github.event.pull_request.head.repo.fork == false
+ run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json
+
+ - name: Publish NuGet packages (fork)
+ if: github.event.pull_request.head.repo.fork == true
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: nupkgs
+ path: src/**/*.nupkg
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..8af59f30
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,100 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL Advanced"
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+ schedule:
+ - cron: "15 3 * * 5"
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: "ubuntu-latest"
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: actions
+ build-mode: none
+ - language: csharp
+ build-mode: none
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ # Add any setup steps before running the `github/codeql-action/init` action.
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
+ # or others). This is typically only required for manual builds.
+ # - name: Setup runtime (example)
+ # uses: actions/setup-example@v1
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/component-owners.yml b/.github/workflows/component-owners.yml
index befef350..359a7b81 100644
--- a/.github/workflows/component-owners.yml
+++ b/.github/workflows/component-owners.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
name: Auto Assign Owners
steps:
- - uses: dyladan/component-owners@a0a1a67d6955b6efe190e9646e0ba536f882414a
+ - uses: dyladan/component-owners@58bd86e9814d23f1525d0a970682cead459fa783
with:
config-file: .github.amrom.workers.devponent_owners.yml
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
index fead00a7..2557a95a 100644
--- a/.github/workflows/dotnet-format.yml
+++ b/.github/workflows/dotnet-format.yml
@@ -2,31 +2,29 @@ name: dotnet format
on:
push:
- branches: [ main ]
- paths:
- - '**.cs'
- - '.editorconfig'
+ branches: [main]
pull_request:
- branches: [ main ]
- paths:
- - '**.cs'
- - '.editorconfig'
+ branches: [main]
jobs:
check-format:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ packages: read
steps:
- - name: Check out code
- uses: actions/checkout@v4
+ - name: Check out code
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- global-json-file: global.json
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ global-json-file: global.json
+ source-url: https://nuget.pkg.github.com/open-feature/index.json
- - name: Install format tool
- run: dotnet tool install -g dotnet-format
-
- - name: dotnet format
- run: dotnet-format --folder --check
+ - name: dotnet format
+ run: dotnet format --verify-no-changes DotnetSdkContrib.sln
diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml
index 6942f814..e761bff6 100644
--- a/.github/workflows/lint-pr.yml
+++ b/.github/workflows/lint-pr.yml
@@ -1,4 +1,4 @@
-name: 'Lint PR'
+name: "Lint PR"
on:
pull_request_target:
@@ -10,13 +10,17 @@ on:
jobs:
main:
name: Validate PR title
+ permissions:
+ contents: read
+ pull-requests: write
runs-on: ubuntu-latest
steps:
- - uses: amannn/action-semantic-pull-request@v5
+ - id: lint_pr_title
+ uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - uses: marocchino/sticky-pull-request-comment@v2
+ - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # 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)
@@ -24,16 +28,16 @@ jobs:
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:
+ uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2
+ with:
header: pr-title-lint-error
delete: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e107b87b..3d484428 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,31 +7,34 @@ on:
jobs:
release-package:
+ environment: publish
runs-on: windows-latest
+ permissions:
+ contents: write
+ pull-requests: write
steps:
- - uses: google-github-actions/release-please-action@v3
+ - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3
id: release
with:
command: manifest
token: ${{secrets.GITHUB_TOKEN}}
default-branch: main
+ release-type: simple
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
if: ${{ steps.release.outputs.releases_created }}
with:
fetch-depth: 0
submodules: recursive
- name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
+ if: ${{ steps.release.outputs.releases_created }}
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
- dotnet-version: |
- 6.0.x
- 7.0.x
- 8.0.x
+ global-json-file: global.json
source-url: https://nuget.pkg.github.com/open-feature/index.json
- name: Install dependencies
@@ -45,7 +48,7 @@ jobs:
- name: Pack
if: ${{ steps.release.outputs.releases_created }}
- run: |
+ run: |
dotnet pack --configuration Release --no-build
- name: Publish to Nuget
diff --git a/.gitmodules b/.gitmodules
index 6940ffbf..b426c70e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,6 @@
[submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"]
path = src/OpenFeature.Contrib.Providers.Flagd/schemas
- url = git@github.com:open-feature/schemas.git
+ url = https://github.com/open-feature/schemas.git
[submodule "spec"]
path = spec
url = https://github.com/open-feature/spec.git
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d9044fb6..d5fde3c1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,9 +1,11 @@
{
"src/OpenFeature.Contrib.Hooks.Otel": "0.2.0",
- "src/OpenFeature.Contrib.Providers.Flagd": "0.3.0",
- "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.0",
+ "src/OpenFeature.Contrib.Providers.Flagd": "0.3.1",
+ "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.1",
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0",
- "src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.5",
+ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1",
"src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0",
- "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0"
+ "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0",
+ "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5",
+ "src/OpenFeature.Contrib.Providers.EnvVar": "0.0.2"
}
\ No newline at end of file
diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln
index 2c8566d1..2aaba701 100644
--- a/DotnetSdkContrib.sln
+++ b/DotnetSdkContrib.sln
@@ -41,6 +41,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig.Test", "test\OpenFeature.Contrib.Providers.Statsig.Test\OpenFeature.Contrib.Providers.Statsig.Test.csproj", "{F3080350-B0AB-4D59-B416-50CC38C99087}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt", "src\OpenFeature.Contrib.Providers.Flipt\OpenFeature.Contrib.Providers.Flipt.csproj", "{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{B446D481-B5A3-4509-8933-C4CF6DA9B147}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.EnvVar", "src\OpenFeature.Contrib.Providers.EnvVar\OpenFeature.Contrib.Providers.EnvVar.csproj", "{F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.EnvVar.Test", "test\OpenFeature.Contrib.Providers.EnvVar.Test\OpenFeature.Contrib.Providers.EnvVar.Test.csproj", "{282AD5C5-099A-403D-B415-29AA88A701EC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +123,22 @@ Global
{F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {282AD5C5-099A-403D-B415-29AA88A701EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {282AD5C5-099A-403D-B415-29AA88A701EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {282AD5C5-099A-403D-B415-29AA88A701EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {282AD5C5-099A-403D-B415-29AA88A701EC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -137,5 +161,9 @@ Global
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
+ {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
+ {B446D481-B5A3-4509-8933-C4CF6DA9B147} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
+ {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
+ {282AD5C5-099A-403D-B415-29AA88A701EC} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
EndGlobal
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 6e4f1a30..cc1a257a 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -1,28 +1,30 @@
-
+
-
- true
- true
- true
- true
-
+
+ true
+ true
+ true
+ true
+
-
- netstandard2.0;net462;net5.0;net6.0;net7.0;net8.0
- git
- https://github.com/open-feature/dotnet-sdk-contrib
- OpenFeature is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution.
- Feature;OpenFeature;Flags;
- openfeature-icon.png
- https://openfeature.dev
- Apache-2.0
- OpenFeature Authors
- true
-
+
+ netstandard2.0;net462;net8.0
+ git
+ https://github.com/open-feature/dotnet-sdk-contrib
+ OpenFeature is an open specification that provides a vendor-agnostic,
+ community-driven API for feature flagging that works with your favorite feature flag
+ management tool or in-house solution.
+ Feature;OpenFeature;Flags;
+ openfeature-icon.png
+ https://openfeature.dev
+ Apache-2.0
+ OpenFeature Authors
+ true
+
-
-
-
+
+
+
-
+
\ No newline at end of file
diff --git a/build/Common.props b/build/Common.props
index 0bc9bed5..bdb03e49 100644
--- a/build/Common.props
+++ b/build/Common.props
@@ -1,33 +1,34 @@
-
-
-
+
+
+
-
- 7.3
- true
-
+
+ latest
+ true
+
-
- full
- true
-
+
+ full
+ true
+
-
- true
-
+
+ true
+
-
-
-
- [2.0,3.0)
-
+
+ [2.0,3.0)
+
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/build/Common.tests.props b/build/Common.tests.props
index 9f94a8e4..0344fc28 100644
--- a/build/Common.tests.props
+++ b/build/Common.tests.props
@@ -1,55 +1,55 @@
-
+
-
- false
- net6.0
- $(TargetFrameworks);net462
-
+
+ false
+ net8.0
+ $(TargetFrameworks);net462
+
-
- true
-
+
+ true
+
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
-
-
- [4.17.0]
- [3.1.2]
- [6.7.0]
- [2.3.3]
- [17.3.2]
- [5.0.0]
- [2.4.3,3.0)
- [2.4.1,3.0)
-
-
+ [4.17.0]
+ [3.1.2]
+ [2.3.3]
+ [17.3.2]
+ [5.0.0]
+ [2.4.3,3.0)
+ [2.4.1,3.0)
+
+
\ No newline at end of file
diff --git a/global.json b/global.json
index bc3a25e7..7b4ac0a9 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,7 @@
{
- "sdk": {
- "rollForward": "latestFeature",
- "version": "8.0.400"
- }
-}
+ "sdk": {
+ "rollForward": "latestFeature",
+ "version": "8.0.408",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
diff --git a/nuget.config b/nuget.config
index 5a0edf43..c5f009d2 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,10 +1,10 @@
-
+
-
+
diff --git a/release-please-config.json b/release-please-config.json
index 08143eca..e9cbda48 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -72,6 +72,26 @@
"extra-files": [
"OpenFeature.Contrib.Providers.Statsig.csproj"
]
+ },
+ "src/OpenFeature.Contrib.Providers.Flipt": {
+ "package-name": "OpenFeature.Contrib.Providers.Flipt",
+ "release-type": "simple",
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": true,
+ "versioning": "default",
+ "extra-files": [
+ "OpenFeature.Contrib.Providers.Flipt.csproj"
+ ]
+ },
+ "src/OpenFeature.Contrib.Providers.EnvVar": {
+ "package-name": "OpenFeature.Contrib.Providers.EnvVar",
+ "release-type": "simple",
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": true,
+ "versioning": "default",
+ "extra-files": [
+ "OpenFeature.Contrib.Providers.EnvVar.csproj"
+ ]
}
},
"changelog-sections": [
diff --git a/renovate.json b/renovate.json
index 0d68c7be..e14ebdb5 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,10 +1,6 @@
{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "config:base"
- ],
- "semanticCommits": "enabled",
- "labels": [
- "renovate"
- ]
-}
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "github>open-feature/community-tooling"
+ ]
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md
index c380cf33..b152b20b 100644
--- a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## [0.1.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.1.0...OpenFeature.Contrib.Providers.ConfigCat-v0.1.1) (2024-09-17)
+
+
+### 🐛 Bug Fixes
+
+* Revise ConfigCat provider ([#280](https://github.com/open-feature/dotnet-sdk-contrib/issues/280)) ([0b2d5f2](https://github.com/open-feature/dotnet-sdk-contrib/commit/0b2d5f29490ad16ee5efde55d31354e0322c6f86))
+
+## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.5...OpenFeature.Contrib.Providers.ConfigCat-v0.1.0) (2024-08-22)
+
+
+### ⚠ BREAKING CHANGES
+
+* use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262))
+
+### ✨ New Features
+
+* use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a))
+
## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.4...OpenFeature.Contrib.Providers.ConfigCat-v0.0.5) (2024-08-21)
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
index 48f966ed..c3889f5f 100644
--- a/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs
@@ -29,6 +29,19 @@ public ConfigCatProvider(string sdkKey, Action configBui
Client = ConfigCatClient.Get(sdkKey, configBuilder);
}
+ ///
+ public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
+ {
+ return Client.WaitForReadyAsync(cancellationToken);
+ }
+
+ ///
+ public override Task ShutdownAsync(CancellationToken cancellationToken = default)
+ {
+ Client.Dispose();
+ return Task.CompletedTask;
+ }
+
///
public override Metadata GetMetadata()
{
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
index 6a9506d3..81025918 100644
--- a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj
@@ -2,7 +2,7 @@
OpenFeature.Contrib.Providers.ConfigCat
- 0.0.5
+ 0.1.1
$(VersionNumber)
$(VersionNumber)
$(VersionNumber)
@@ -16,6 +16,6 @@
-
+
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/README.md b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md
index b08e8826..227f7680 100644
--- a/src/OpenFeature.Contrib.Providers.ConfigCat/README.md
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md
@@ -2,7 +2,7 @@
The ConfigCat Flag provider allows you to connect to your ConfigCat instance.
-# .Net SDK usage
+# .NET SDK usage
## Requirements
@@ -47,68 +47,72 @@ paket add OpenFeature.Contrib.Providers.ConfigCat
The following example shows how to use the ConfigCat provider with the OpenFeature SDK.
```csharp
-using OpenFeature.Contrib.Providers.ConfigCat;
+using System;
+using ConfigCat.Client;
+using OpenFeature.Contrib.ConfigCat;
-namespace OpenFeatureTestApp
+var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");
+
+// Set the configCatProvider as the provider for the OpenFeature SDK
+await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider);
+
+var client = OpenFeature.Api.Instance.GetClient();
+
+var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false);
+if (isAwesomeFeatureEnabled)
+{
+ doTheNewThing();
+}
+else
{
- class Hello {
- static void Main(string[] args) {
- var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#");
-
- // Set the configCatProvider as the provider for the OpenFeature SDK
- OpenFeature.Api.Instance.SetProvider(configCatProvider);
-
- var client = OpenFeature.Api.Instance.GetClient();
-
- var val = client.GetBooleanValueAsync("isMyAwesomeFeatureEnabled", false);
-
- if(isMyAwesomeFeatureEnabled)
- {
- doTheNewThing();
- }
- else
- {
- doTheOldThing();
- }
- }
- }
+ doTheOldThing();
}
```
### Customizing the ConfigCat Provider
-The ConfigCat provider can be customized by passing a `ConfigCatClientOptions` object to the constructor.
+The ConfigCat provider can be customized by passing a callback setting up a `ConfigCatClientOptions` object to the constructor.
```csharp
-var configCatOptions = new ConfigCatClientOptions
+Action configureConfigCatOptions = (options) =>
{
- PollingMode = PollingModes.ManualPoll;
- Logger = new ConsoleLogger(LogLevel.Info);
+ options.PollingMode = PollingModes.LazyLoad(cacheTimeToLive: TimeSpan.FromSeconds(10));
+ options.Logger = new ConsoleLogger(LogLevel.Info);
+ // ...
};
-var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configCatOptions);
+var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configureConfigCatOptions);
```
For a full list of options see the [ConfigCat documentation](https://configcat.com/docs/sdk-reference/dotnet/).
-## EvaluationContext and ConfigCat User relationship
+### Cleaning up
+
+On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client.
+
+```csharp
+await OpenFeature.Api.Instance.ShutdownAsync();
+```
+
+## EvaluationContext and ConfigCat User Object relationship
-ConfigCat has the concept of Users where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The ConfigCat provider will map the EvaluationContext to a ConfigCat User.
+An evaluation context in the OpenFeature specification is a container for arbitrary contextual data that can be used as a basis for feature flag evaluation.
+The ConfigCat provider translates these evaluation contexts to ConfigCat [User Objects](https://configcat.com/docs/targeting/user-object/).
-The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are:
+The ConfigCat User Object has a few pre-defined attributes that can be used to evaluate a flag. These are:
-| Parameter | Description |
-|-----------|---------------------------------------------------------------------------------------------------------------------------------|
-| `Id` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
-| `Email` | Optional parameter for easier targeting rule definitions. |
-| `Country` | Optional parameter for easier targeting rule definitions. |
-| `Custom` | Optional dictionary for custom attributes of a user for advanced targeting rule definitions. E.g. User role, Subscription type. |
+| Attribute | Description |
+|--------------|----------------------------------------------------------------------------------------------------------------|
+| `Identifier` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. |
+| `Email` | The email address of the user. |
+| `Country` | The country of the user. |
-Since EvaluationContext is a simple dictionary, the provider will try to match the keys to the ConfigCat User parameters following the table below in a case-insensitive manner.
+Since `EvaluationContext` is a simple dictionary, the provider will try to match the keys to ConfigCat user attributes following the table below in a case-insensitive manner.
-| EvaluationContext Key | ConfigCat User Parameter |
+| EvaluationContext Key | ConfigCat User Attribute |
|-----------------------|--------------------------|
-| `id` | `Id` |
-| `identifier` | `Id` |
+| `id` | `Identifier` |
+| `identifier` | `Identifier` |
| `email` | `Email` |
-| `country` | `Country` |
\ No newline at end of file
+| `country` | `Country` |
+| Any other | `Custom` |
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
index ba8798ee..391e487b 100644
--- a/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
+++ b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using ConfigCat.Client;
using OpenFeature.Model;
@@ -17,35 +16,32 @@ internal static User BuildUser(this EvaluationContext context)
return null;
}
- var user = context.TryGetValuesInsensitive(PossibleUserIds, out var pair)
- ? new User(pair.Value.AsString)
- : new User(Guid.NewGuid().ToString());
+ var user = new User(context.GetUserId());
foreach (var value in context)
{
- switch (value.Key.ToUpperInvariant())
+ if (StringComparer.OrdinalIgnoreCase.Equals("EMAIL", value.Key))
{
- case "EMAIL":
- user.Email = value.Value.AsString;
- continue;
- case "COUNTRY":
- user.Country = value.Value.AsString;
- continue;
- default:
- user.Custom.Add(value.Key, value.Value.AsString);
- continue;
+ user.Email = value.Value.AsString;
+ }
+ else if (StringComparer.OrdinalIgnoreCase.Equals("COUNTRY", value.Key))
+ {
+ user.Country = value.Value.AsString;
+ }
+ else
+ {
+ user.Custom.Add(value.Key, value.Value.AsString);
}
}
return user;
}
- private static bool TryGetValuesInsensitive(this EvaluationContext context, string[] keys,
- out KeyValuePair pair)
+ private static string GetUserId(this EvaluationContext context)
{
- pair = context.AsDictionary().FirstOrDefault(x => keys.Contains(x.Key.ToUpperInvariant()));
+ var pair = context.AsDictionary().FirstOrDefault(x => PossibleUserIds.Contains(x.Key, StringComparer.OrdinalIgnoreCase));
- return pair.Key != null;
+ return pair.Key != null ? pair.Value.AsString : "";
}
}
}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md
new file mode 100644
index 00000000..a64575be
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.1...OpenFeature.Contrib.Providers.EnvVar-v0.0.2) (2025-03-12)
+
+
+### ✨ New Features
+
+* Environment Variable Provider ([#312](https://github.com/open-feature/dotnet-sdk-contrib/issues/312)) ([4908000](https://github.com/open-feature/dotnet-sdk-contrib/commit/4908000ed27a648ee7cf8823320ae7d7c8cd8c45))
diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs b/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs
new file mode 100644
index 00000000..36056958
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.EnvVar
+{
+ ///
+ /// An OpenFeature provider using environment variables.
+ ///
+ public sealed class EnvVarProvider : FeatureProvider
+ {
+ private const string Name = "Environment Variable Provider";
+ private readonly string _prefix;
+ private delegate bool TryConvert(string value, out TResult result);
+
+ ///
+ /// Creates a new instance of
+ ///
+ public EnvVarProvider() : this(string.Empty)
+ {
+ }
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// A prefix which will be used when evaluating environment variables
+ public EnvVarProvider(string prefix)
+ {
+ _prefix = prefix;
+ }
+
+ ///
+ public override Metadata GetMetadata()
+ {
+ return new Metadata(Name);
+ }
+
+ private Task> Resolve(string flagKey, T defaultValue, TryConvert tryConvert)
+ {
+ var envVarName = $"{_prefix}{flagKey}";
+ var value = Environment.GetEnvironmentVariable(envVarName);
+
+ if (value == null)
+ return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error, string.Empty, $"Unable to find environment variable '{envVarName}'"));
+
+ if (!tryConvert(value, out var convertedValue))
+ throw new FeatureProviderException(ErrorType.TypeMismatch, $"Could not convert the value of environment variable '{envVarName}' to {typeof(T)}");
+
+ return Task.FromResult(new ResolutionDetails(flagKey, convertedValue, ErrorType.None, Reason.Static));
+ }
+
+ ///
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new CancellationToken())
+ {
+ return Resolve(flagKey, defaultValue, bool.TryParse);
+ }
+
+ ///
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new CancellationToken())
+ {
+ return Resolve(flagKey, defaultValue, NoopTryParse);
+
+ bool NoopTryParse(string value, out string result)
+ {
+ result = value;
+ return true;
+ }
+ }
+
+ ///
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new CancellationToken())
+ {
+ return Resolve(flagKey, defaultValue, int.TryParse);
+ }
+
+ ///
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new CancellationToken())
+ {
+ return Resolve(flagKey, defaultValue, double.TryParse);
+ }
+
+ ///
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new CancellationToken())
+ {
+ return Resolve(flagKey, defaultValue, ConvertStringToValue);
+
+ bool ConvertStringToValue(string s, out Value value)
+ {
+ value = new Value(s);
+ return true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj
new file mode 100644
index 00000000..86f2754a
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj
@@ -0,0 +1,13 @@
+
+
+
+ OpenFeature.Contrib.Providers.EnvVar
+ 0.0.2
+ $(VersionNumber)
+ $(VersionNumber)
+ $(VersionNumber)
+ Environment Variable Provider for .NET
+ Octopus Deploy
+
+
+
diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/README.md b/src/OpenFeature.Contrib.Providers.EnvVar/README.md
new file mode 100644
index 00000000..7b1645b7
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.EnvVar/README.md
@@ -0,0 +1,41 @@
+# .NET Environment Variable Provider
+
+This provider supports using the OpenFeature SDK to evaluate feature flags backed by environment variables.
+
+## Installation
+
+### .NET CLI
+
+```shell
+dotnet add package OpenFeature.Contrib.Providers.EnvVar
+```
+
+## Using the ConfigCat Provider with the OpenFeature SDK
+
+The following example shows how to use the Environment Variable provider with the OpenFeature SDK.
+
+```csharp
+using System;
+using OpenFeature;
+using OpenFeature.Contrib.EnvVar;
+
+// If you want to use a prefix for your environment variables, you can supply it in the constructor below.
+// For example, if you all your feature flag environment variables will be prefixed with feature-flag- then
+// you would use:
+// var envVarProvider = new EnvVarProvider("feature-flag-");
+var envVarProvider = new EnvVarProvider();
+
+// Set the Environment Variable provider as the provider for the OpenFeature SDK
+await OpenFeature.Api.Instance.SetProviderAsync(envVarProvider);
+var client = OpenFeature.Api.Instance.GetClient();
+
+var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false);
+if (isAwesomeFeatureEnabled)
+{
+ doTheNewThing();
+}
+else
+{
+ doTheOldThing();
+}
+```
diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/version.txt b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt
new file mode 100644
index 00000000..4e379d2b
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt
@@ -0,0 +1 @@
+0.0.2
diff --git a/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj b/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj
index 4f5c1578..3bdcb8ca 100644
--- a/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj
+++ b/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md
index 693fc5a8..2b0b8c82 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md
+++ b/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md
@@ -1,5 +1,35 @@
# Changelog
+## [0.3.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flagd-v0.3.0...OpenFeature.Contrib.Providers.Flagd-v0.3.1) (2025-04-22)
+
+
+### 🐛 Bug Fixes
+
+* migrate to System.Text.Json and JsonLogic ([#347](https://github.com/open-feature/dotnet-sdk-contrib/issues/347)) ([ef98686](https://github.com/open-feature/dotnet-sdk-contrib/commit/ef9868688f0804e26a1b69b6ea25be5f105c26b5))
+
+
+### ✨ New Features
+
+* Update in-process resolver to support flag metadata [#305](https://github.com/open-feature/dotnet-sdk-contrib/issues/305) ([#309](https://github.com/open-feature/dotnet-sdk-contrib/issues/309)) ([e603c08](https://github.com/open-feature/dotnet-sdk-contrib/commit/e603c08df7c19f360b2d8896caef3e3a5bcdcefd))
+
+
+### 🧹 Chore
+
+* **deps:** update dependency google.protobuf to 3.28.2 ([#272](https://github.com/open-feature/dotnet-sdk-contrib/issues/272)) ([1c45c1a](https://github.com/open-feature/dotnet-sdk-contrib/commit/1c45c1a3578ddc814483ac83549c2be5579d403c))
+* **deps:** update dependency google.protobuf to 3.30.2 ([#335](https://github.com/open-feature/dotnet-sdk-contrib/issues/335)) ([3f63d35](https://github.com/open-feature/dotnet-sdk-contrib/commit/3f63d35540979dfb42e1f9d80ba5d2bba0b4a509))
+* **deps:** update dependency grpc.net.client to 2.66.0 ([#282](https://github.com/open-feature/dotnet-sdk-contrib/issues/282)) ([04803d7](https://github.com/open-feature/dotnet-sdk-contrib/commit/04803d7cfcf739ea17c11dc576444ae75ba85192))
+* **deps:** update dependency grpc.net.client to 2.70.0 ([#336](https://github.com/open-feature/dotnet-sdk-contrib/issues/336)) ([cd4cd4f](https://github.com/open-feature/dotnet-sdk-contrib/commit/cd4cd4f29bedebcca0a11085307bed72e6e7b794))
+* **deps:** update dependency grpc.tools to 2.66.0 ([#271](https://github.com/open-feature/dotnet-sdk-contrib/issues/271)) ([161fb63](https://github.com/open-feature/dotnet-sdk-contrib/commit/161fb638f22eecae2d4caa84c6c595878c8c48c9))
+* **deps:** update dependency grpc.tools to 2.71.0 ([#286](https://github.com/open-feature/dotnet-sdk-contrib/issues/286)) ([84acae2](https://github.com/open-feature/dotnet-sdk-contrib/commit/84acae2663677cf60c7e9691fb22fd250af6fd64))
+* **deps:** update dependency semver to v3 ([#351](https://github.com/open-feature/dotnet-sdk-contrib/issues/351)) ([9f47608](https://github.com/open-feature/dotnet-sdk-contrib/commit/9f4760807f6d5ddf416a8ec7bd931f698f4f30b2))
+* **deps:** update ghcr.io/open-feature/flagd-testbed docker tag to v0.5.21 ([#291](https://github.com/open-feature/dotnet-sdk-contrib/issues/291)) ([29553b2](https://github.com/open-feature/dotnet-sdk-contrib/commit/29553b252344057dc4eba7379b95acb085e9caa1))
+* **deps:** update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v0.5.21 ([#323](https://github.com/open-feature/dotnet-sdk-contrib/issues/323)) ([faa44cc](https://github.com/open-feature/dotnet-sdk-contrib/commit/faa44cc6db5b014069f3dd72b1bf34e3e5ada1df))
+* **deps:** update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v1 ([#339](https://github.com/open-feature/dotnet-sdk-contrib/issues/339)) ([308cc42](https://github.com/open-feature/dotnet-sdk-contrib/commit/308cc42afce6196ff4c4ffc89350454a44f1d1e0))
+* **deps:** update ghcr.io/open-feature/sync-testbed-unstable docker tag to v0.5.13 ([#333](https://github.com/open-feature/dotnet-sdk-contrib/issues/333)) ([6cbf656](https://github.com/open-feature/dotnet-sdk-contrib/commit/6cbf6563e7f5b2f4c4b8e0b557f978cfe12f79c9))
+* **deps:** update src/openfeature.contrib.providers.flagd/schemas digest to 9b0ee43 ([#332](https://github.com/open-feature/dotnet-sdk-contrib/issues/332)) ([1f7214d](https://github.com/open-feature/dotnet-sdk-contrib/commit/1f7214d28f04e504fdf8f3dac7fa14ff613fa677))
+* **deps:** update src/openfeature.contrib.providers.flagd/schemas digest to c707f56 ([#343](https://github.com/open-feature/dotnet-sdk-contrib/issues/343)) ([5d142fd](https://github.com/open-feature/dotnet-sdk-contrib/commit/5d142fd798da9b668d8b45f5f6310a03b1424c36))
+* Use TestContainers instead of github services / docker for e2e tests ([#345](https://github.com/open-feature/dotnet-sdk-contrib/issues/345)) ([1173f4f](https://github.com/open-feature/dotnet-sdk-contrib/commit/1173f4f1c0a06f191d4aa6b0353ac54f81889ec6))
+
## [0.3.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flagd-v0.2.3...OpenFeature.Contrib.Providers.Flagd-v0.3.0) (2024-08-22)
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs
index b8512dd9..da45a056 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs
@@ -1,6 +1,4 @@
using System;
-using System.Numerics;
-using JsonLogic.Net;
namespace OpenFeature.Contrib.Providers.Flagd
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs
index 2b12223c..40a966f9 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs
@@ -116,7 +116,7 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
if (t.IsFaulted)
{
throw t.Exception;
- };
+ }
});
}
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj
index 049033b0..22d1ee73 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj
+++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj
@@ -2,7 +2,7 @@
OpenFeature.Contrib.Providers.Flagd
- 0.3.0
+ 0.3.1
$(VersionNumber)
$(VersionNumber)
$(VersionNumber)
@@ -22,16 +22,17 @@
-
+
-
+
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs
index ce336ac9..116e8a97 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs
@@ -1,8 +1,12 @@
using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
+using Json.More;
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators
{
- internal class FlagdProperties
+ internal sealed class FlagdProperties
{
internal const string FlagdPropertiesKey = "$flagd";
@@ -14,30 +18,23 @@ internal class FlagdProperties
internal long Timestamp { get; set; }
internal string TargetingKey { get; set; }
- internal FlagdProperties(object from)
+ internal FlagdProperties(EvaluationContext from)
{
- //object value;
- if (from is IDictionary dict)
+
+ if (from.TryFind(TargetingKeyKey, out JsonNode targetingKeyValue)
+ && targetingKeyValue.GetValueKind() == JsonValueKind.String)
+ {
+ TargetingKey = targetingKeyValue.ToString();
+ }
+ if (from.TryFind($"{FlagdPropertiesKey}.{FlagKeyKey}", out JsonNode flagKeyValue)
+ && flagKeyValue.GetValueKind() == JsonValueKind.String)
+ {
+ FlagKey = flagKeyValue.ToString();
+ }
+ if (from.TryFind($"{FlagdPropertiesKey}.{TimestampKey}", out JsonNode timestampValue)
+ && timestampValue.GetValueKind() == JsonValueKind.Number)
{
- if (dict.TryGetValue(TargetingKeyKey, out object targetingKeyValue)
- && targetingKeyValue is string targetingKeyString)
- {
- TargetingKey = targetingKeyString;
- }
- if (dict.TryGetValue(FlagdPropertiesKey, out object flagdPropertiesObj)
- && flagdPropertiesObj is IDictionary flagdProperties)
- {
- if (flagdProperties.TryGetValue(FlagKeyKey, out object flagKeyObj)
- && flagKeyObj is string flagKey)
- {
- FlagKey = flagKey;
- }
- if (flagdProperties.TryGetValue(TimestampKey, out object timestampObj)
- && timestampObj is long timestamp)
- {
- Timestamp = timestamp;
- }
- }
+ Timestamp = timestampValue.GetValue();
}
}
}
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs
similarity index 70%
rename from src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs
rename to src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs
index f685d3c2..d59cab41 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs
@@ -2,47 +2,44 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
-using JsonLogic.Net;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
using Murmur;
-using Newtonsoft.Json.Linq;
-using Semver;
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators
{
///
- public class FractionalEvaluator
+ internal sealed class FractionalEvaluator : IRule
{
- internal FractionalEvaluator()
- {
- }
-
class FractionalEvaluationDistribution
{
public string variant;
public int weight;
}
- internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data)
+ ///
+ public JsonNode Apply(JsonNode args, EvaluationContext context)
{
// check if we have at least two arguments:
// 1. the property value
// 2. the array containing the buckets
- if (args.Length == 0)
+ if (args.AsArray().Count == 0)
{
return null;
}
- var flagdProperties = new FlagdProperties(data);
+ var flagdProperties = new FlagdProperties(context);
var bucketStartIndex = 0;
- var arg0 = p.Apply(args[0], data);
+ var arg0 = JsonLogic.Apply(args[0], context);
string propertyValue;
- if (arg0 is string stringValue)
+ if (arg0.GetValueKind() == JsonValueKind.String)
{
- propertyValue = stringValue;
+ propertyValue = arg0.ToString();
bucketStartIndex = 1;
}
else
@@ -53,16 +50,16 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data)
var distributions = new List();
var distributionSum = 0;
- for (var i = bucketStartIndex; i < args.Length; i++)
+ for (var i = bucketStartIndex; i < args.AsArray().Count; i++)
{
- var bucket = p.Apply(args[i], data);
+ var bucket = JsonLogic.Apply(args[i], context);
- if (!bucket.IsEnumerable())
+ if (!(bucket.GetValueKind() == JsonValueKind.Array))
{
continue;
}
- var bucketArr = bucket.MakeEnumerable().ToArray();
+ var bucketArr = bucket.AsArray();
if (!bucketArr.Any())
{
@@ -71,9 +68,9 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data)
var weight = 1;
- if (bucketArr.Length >= 2 && bucketArr.ElementAt(1).IsNumeric())
+ if (bucketArr.Count >= 2 && bucketArr.ElementAt(1).GetValueKind() == JsonValueKind.Number)
{
- weight = Convert.ToInt32(bucketArr.ElementAt(1));
+ weight = bucketArr.ElementAt(1).GetValue();
}
distributions.Add(new FractionalEvaluationDistribution
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs
similarity index 81%
rename from src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs
rename to src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs
index a25c968d..5d591259 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs
@@ -1,17 +1,12 @@
-using System;
-using JsonLogic.Net;
-using Newtonsoft.Json.Linq;
+using System.Text.Json.Nodes;
+using Json.Logic;
using Semver;
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators
{
///
- public class SemVerEvaluator
+ internal sealed class SemVerRule : IRule
{
- internal SemVerEvaluator()
- {
- }
-
const string OperatorEqual = "=";
const string OperatorNotEqual = "!=";
const string OperatorLess = "<";
@@ -21,21 +16,23 @@ internal SemVerEvaluator()
const string OperatorMatchMajor = "^";
const string OperatorMatchMinor = "~";
- internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data)
+
+ ///
+ public JsonNode Apply(JsonNode args, EvaluationContext context)
{
// check if we have at least 3 arguments
- if (args.Length < 3)
+ if (args.AsArray().Count < 3)
{
return false;
}
// get the value from the provided evaluation context
- var versionString = p.Apply(args[0], data).ToString();
+ var versionString = JsonLogic.Apply(args[0], context).ToString();
// get the operator
- var semVerOperator = p.Apply(args[1], data).ToString();
+ var semVerOperator = JsonLogic.Apply(args[1], context).ToString();
// get the target version
- var targetVersionString = p.Apply(args[2], data).ToString();
+ var targetVersionString = JsonLogic.Apply(args[2], context).ToString();
//convert to semantic versions
if (!SemVersion.TryParse(versionString, SemVersionStyles.Strict, out var version) ||
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs
deleted file mode 100644
index f6119378..00000000
--- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using JsonLogic.Net;
-using Newtonsoft.Json.Linq;
-using OpenFeature.Error;
-using OpenFeature.Model;
-
-namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators
-{
- internal class StringEvaluator
- {
- internal StringEvaluator()
- {
- }
-
- internal object StartsWith(IProcessJsonLogic p, JToken[] args, object data)
- {
- if (!isValid(p, args, data, out string operandA, out string operandB))
- {
- return false;
- };
- return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB));
- }
-
- internal object EndsWith(IProcessJsonLogic p, JToken[] args, object data)
- {
- if (!isValid(p, args, data, out string operandA, out string operandB))
- {
- return false;
- };
- return operandA.EndsWith(operandB);
- }
-
- private bool isValid(IProcessJsonLogic p, JToken[] args, object data, out string operandA, out string operandB)
- {
- // check if we have at least 2 arguments
- operandA = null;
- operandB = null;
-
- if (args.Length < 2)
- {
- return false;
- }
- operandA = p.Apply(args[0], data) as string;
- operandB = p.Apply(args[1], data) as string;
-
- if (!(operandA is string) || !(operandB is string))
- {
- // return false immediately if both operands are not strings.
- return false;
- }
-
- Convert.ToString(operandA);
- Convert.ToString(operandB);
-
- return true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs
new file mode 100644
index 00000000..fd5eceed
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
+
+namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators
+{
+ internal sealed class StartsWithRule : IRule
+ {
+ public JsonNode Apply(JsonNode args, Json.Logic.EvaluationContext context)
+ {
+ if (!StringRule.isValid(args, context, out string operandA, out string operandB))
+ {
+ return false;
+ }
+ return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB));
+ }
+ }
+
+ internal sealed class EndsWithRule : IRule
+ {
+ public JsonNode Apply(JsonNode args, Json.Logic.EvaluationContext context)
+ {
+ if (!StringRule.isValid(args, context, out string operandA, out string operandB))
+ {
+ return false;
+ }
+ return operandA.EndsWith(operandB);
+ }
+ }
+
+ internal static class StringRule
+ {
+ internal static bool isValid(JsonNode args, Json.Logic.EvaluationContext context, out string argA, out string argB)
+ {
+ argA = null;
+ argB = null;
+
+ // check if we have at least 2 arguments
+ if (args.AsArray().Count < 2)
+ {
+ return false;
+ }
+
+ var nodeA = JsonLogic.Apply(args[0], context);
+ var nodeB = JsonLogic.Apply(args[1], context);
+
+ // return false immediately if both operands are not strings
+ if (nodeA?.GetValueKind() != JsonValueKind.String || nodeB?.GetValueKind() != JsonValueKind.String)
+ {
+ return false;
+ }
+
+ argA = nodeA.ToString();
+ argB = nodeB.ToString();
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs
index 6154982e..07117d23 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs
+++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs
@@ -2,38 +2,35 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
-using JsonLogic.Net;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+using Json.Logic;
+using Json.More;
using OpenFeature.Constant;
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
using OpenFeature.Error;
using OpenFeature.Model;
-using System.Text.RegularExpressions;
+using EvaluationContext = OpenFeature.Model.EvaluationContext;
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess
{
-
internal class FlagConfiguration
{
- [JsonProperty("state")]
- internal string State { get; set; }
- [JsonProperty("defaultVariant")]
- internal string DefaultVariant { get; set; }
- [JsonProperty("variants")]
- internal Dictionary Variants { get; set; }
- [JsonProperty("targeting")]
- internal object Targeting { get; set; }
- [JsonProperty("source")]
- internal string Source { get; set; }
+ [JsonPropertyName("state")] public string State { get; set; }
+ [JsonPropertyName("defaultVariant")] public string DefaultVariant { get; set; }
+ [JsonPropertyName("variants")] public Dictionary Variants { get; set; }
+ [JsonPropertyName("targeting")] public object Targeting { get; set; }
+ [JsonPropertyName("source")] public string Source { get; set; }
+ [JsonPropertyName("metadata")] public Dictionary Metadata { get; set; }
}
internal class FlagSyncData
{
- [JsonProperty("flags")]
- internal Dictionary Flags { get; set; }
- [JsonProperty("$evaluators")]
- internal Dictionary Evaluators { get; set; }
+ [JsonPropertyName("flags")] public Dictionary Flags { get; set; }
+ [JsonPropertyName("$evaluators")] public Dictionary Evaluators { get; set; }
+ [JsonPropertyName("metadata")] public Dictionary Metadata { get; set; }
}
internal class FlagConfigurationSync
@@ -53,30 +50,25 @@ internal enum FlagConfigurationUpdateType
internal class JsonEvaluator
{
private Dictionary _flags = new Dictionary();
+ private Dictionary _flagSetMetadata = new Dictionary();
private string _selector;
- private readonly JsonLogicEvaluator _evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
-
internal JsonEvaluator(string selector)
{
_selector = selector;
- var stringEvaluator = new StringEvaluator();
- var semVerEvaluator = new SemVerEvaluator();
- var fractionalEvaluator = new FractionalEvaluator();
-
- EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.StartsWith);
- EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith);
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
- EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate);
+ RuleRegistry.AddRule("starts_with", new StartsWithRule());
+ RuleRegistry.AddRule("ends_with", new EndsWithRule());
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
+ RuleRegistry.AddRule("fractional", new FractionalEvaluator());
}
internal FlagSyncData Parse(string flagConfigurations)
{
- var parsed = JsonConvert.DeserializeObject(flagConfigurations);
- var transformed = JsonConvert.SerializeObject(parsed);
+ var parsed = JsonSerializer.Deserialize(flagConfigurations);
+ var transformed = JsonSerializer.Serialize(parsed);
// replace evaluators
if (parsed.Evaluators != null && parsed.Evaluators.Count > 0)
{
@@ -88,7 +80,68 @@ internal FlagSyncData Parse(string flagConfigurations)
});
}
- return JsonConvert.DeserializeObject(transformed);
+
+ var data = JsonSerializer.Deserialize(transformed);
+ if (data.Metadata == null)
+ {
+ data.Metadata = new Dictionary();
+ }
+ else
+ {
+ foreach (var key in new List(data.Metadata.Keys))
+ {
+ var value = data.Metadata[key];
+ VerifyMetadataValue(key, value);
+ }
+ }
+
+ foreach (var flagConfig in data.Flags)
+ {
+ if (flagConfig.Value.Metadata == null)
+ {
+ continue;
+ }
+
+ foreach (var key in new List(flagConfig.Value.Metadata.Keys))
+ {
+ var value = flagConfig.Value.Metadata[key];
+ VerifyMetadataValue(key, value);
+ }
+ }
+
+ return data;
+ }
+
+ private static void VerifyMetadataValue(string key, JsonElement value)
+ {
+ //if (value is int || value is double || value is string || value is bool)
+ if (value.ValueKind == JsonValueKind.Number
+ || value.ValueKind == JsonValueKind.String
+ || value.ValueKind == JsonValueKind.True
+ || value.ValueKind == JsonValueKind.False)
+ {
+ return;
+ }
+
+ throw new ParseErrorException("Metadata entry for key " + key + " and value " + value +
+ " is of unknown type");
+ }
+
+ private static object ExtractMetadataValue(string key, JsonElement value)
+ {
+ switch (value.ValueKind)
+ {
+ case JsonValueKind.Number:
+ return value.GetDouble();
+ case JsonValueKind.String:
+ return value.GetString();
+ case JsonValueKind.False:
+ case JsonValueKind.True:
+ return value.GetBoolean();
+
+ }
+ throw new ParseErrorException("Metadata entry for key " + key + " and value " + value +
+ " is of unknown type");
}
internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations)
@@ -99,55 +152,69 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat
{
case FlagConfigurationUpdateType.ALL:
_flags = flagConfigsMap.Flags;
+ _flagSetMetadata = flagConfigsMap.Metadata;
+
break;
case FlagConfigurationUpdateType.ADD:
+ case FlagConfigurationUpdateType.UPDATE:
foreach (var keyAndValue in flagConfigsMap.Flags)
{
_flags[keyAndValue.Key] = keyAndValue.Value;
}
- break;
- case FlagConfigurationUpdateType.UPDATE:
- foreach (var keyAndValue in flagConfigsMap.Flags)
+
+ foreach (var metadata in flagConfigsMap.Metadata)
{
- _flags[keyAndValue.Key] = keyAndValue.Value;
+ _flagSetMetadata[metadata.Key] = metadata.Value;
}
+
break;
case FlagConfigurationUpdateType.DELETE:
foreach (var keyAndValue in flagConfigsMap.Flags)
{
_flags.Remove(keyAndValue.Key);
}
- break;
+ foreach (var keyValuePair in flagConfigsMap.Metadata)
+ {
+ _flagSetMetadata.Remove(keyValuePair.Key);
+ }
+
+ break;
}
}
- public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null)
+ public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue,
+ EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}
- public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null)
+ public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue,
+ EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}
- public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null)
+ public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue,
+ EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}
- public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null)
+ public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue,
+ EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}
- public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null)
+ public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue,
+ EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}
- private ResolutionDetails ResolveValue(string flagKey, T defaultValue, EvaluationContext context = null)
+ private ResolutionDetails ResolveValue(string flagKey, T defaultValue,
+ EvaluationContext context = null)
{
// check if we find the flag key
var reason = Reason.Static;
@@ -155,36 +222,54 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva
{
if ("DISABLED" == flagConfiguration.State)
{
- throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
+ throw new FeatureProviderException(ErrorType.FlagNotFound,
+ "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
+ }
+
+ Dictionary combinedMetadata = _flagSetMetadata.ToDictionary(
+ entry => entry.Key,
+ entry => ExtractMetadataValue(entry.Key, entry.Value));
+
+ if (flagConfiguration.Metadata != null)
+ {
+ foreach (var metadataEntry in flagConfiguration.Metadata)
+ {
+ combinedMetadata[metadataEntry.Key] = ExtractMetadataValue(metadataEntry.Key, metadataEntry.Value);
+ }
}
+
+ var flagMetadata = new ImmutableMetadata(combinedMetadata);
var variant = flagConfiguration.DefaultVariant;
- if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}")
+ if (flagConfiguration.Targeting != null &&
+ !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) &&
+ flagConfiguration.Targeting.ToString() != "{}")
{
reason = Reason.TargetingMatch;
- var flagdProperties = new Dictionary();
- flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey));
- flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
+ var flagdProperties = new Dictionary
+ {
+ { FlagdProperties.FlagKeyKey, new Value(flagKey) },
+ { FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()) }
+ };
if (context == null)
{
context = EvaluationContext.Builder().Build();
}
- var targetingContext = context.AsDictionary().Add(
- FlagdProperties.FlagdPropertiesKey,
- new Value(new Structure(flagdProperties))
- );
+ var contextDictionary = context.AsDictionary();
+ contextDictionary = contextDictionary.Add(FlagdProperties.FlagdPropertiesKey, new Value(new Structure(flagdProperties)));
+ // TODO: all missing comments
var targetingString = flagConfiguration.Targeting.ToString();
// Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
// the JsonLogic evaluator will return the variant for the value
// convert the EvaluationContext object into something the JsonLogic evaluator can work with
- dynamic contextObj = (object)ConvertToDynamicObject(targetingContext);
+ var contextObj = JsonNode.Parse(JsonSerializer.Serialize(ConvertToDynamicObject(contextDictionary)));
// convert whatever is returned to a string to try to use it as an index to Variants
- var ruleResult = _evaluator.Apply(rule, contextObj);
+ var ruleResult = JsonLogic.Apply(rule, contextObj);
if (ruleResult is bool)
{
// if this was a bool, convert from "True" to "true" to match JSON
@@ -202,53 +287,82 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva
{
// if variant is null, revert to default
reason = Reason.Default;
- flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue);
- if (defaultVariantValue == null)
+ flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant,
+ out var defaultVariantValue);
+ if (defaultVariantValue.ValueKind == JsonValueKind.Undefined || defaultVariantValue.ValueKind == JsonValueKind.Null)
{
- throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
+ throw new FeatureProviderException(ErrorType.ParseError,
+ "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
}
+
var value = ExtractFoundVariant(defaultVariantValue, flagKey);
return new ResolutionDetails(
- flagKey: flagKey,
- value,
- reason: reason,
- variant: variant
- );
+ flagKey: flagKey,
+ value,
+ reason: reason,
+ variant: variant,
+ flagMetadata: flagMetadata
+ );
}
else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue))
{
// if variant can be found, return it - this could be TARGETING_MATCH or STATIC.
var value = ExtractFoundVariant(foundVariantValue, flagKey);
return new ResolutionDetails(
- flagKey: flagKey,
- value,
- reason: reason,
- variant: variant
- );
+ flagKey: flagKey,
+ value,
+ reason: reason,
+ variant: variant,
+ flagMetadata: flagMetadata
+ );
}
}
- throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found");
+
+ throw new FeatureProviderException(ErrorType.FlagNotFound,
+ "FLAG_NOT_FOUND: flag '" + flagKey + "' not found");
}
- static T ExtractFoundVariant(object foundVariantValue, string flagKey)
+ static T ExtractFoundVariant(JsonElement foundVariantValue, string flagKey)
{
- if (foundVariantValue is long)
- {
- foundVariantValue = Convert.ToInt32(foundVariantValue);
- }
- if (typeof(T) == typeof(double))
- {
- foundVariantValue = Convert.ToDouble(foundVariantValue);
- }
- else if (foundVariantValue is JObject value)
+ try
{
- foundVariantValue = ConvertJObjectToOpenFeatureValue(value);
+ if (typeof(T) == typeof(int))
+ {
+ return (T)(object)foundVariantValue.GetInt32();
+ }
+
+ if (typeof(T) == typeof(double))
+ {
+ return (T)(object)foundVariantValue.GetDouble();
+ }
+
+ if (typeof(T) == typeof(bool))
+ {
+ return (T)(object)foundVariantValue.GetBoolean();
+ }
+
+ if (typeof(T) == typeof(string))
+ {
+ return (T)(object)foundVariantValue.GetString();
+ }
+
+ if (foundVariantValue.ValueKind == JsonValueKind.Object || foundVariantValue.ValueKind == JsonValueKind.Array)
+ {
+ var converted = ConvertJsonObjectToOpenFeatureValue(foundVariantValue.AsNode().AsObject());
+ if (converted is T castValue)
+ {
+ return castValue;
+ }
+ }
+ throw new Exception("Cannot cast flag value to expected type");
+
}
- if (foundVariantValue is T castValue)
+ catch (Exception e)
{
- return castValue;
+ throw new FeatureProviderException(ErrorType.TypeMismatch,
+ "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type", e);
}
- throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type");
+
}
static dynamic ConvertToDynamicObject(IImmutableDictionary dictionary)
@@ -259,47 +373,47 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary dictio
foreach (var kvp in dictionary)
{
expandoDict.Add(kvp.Key,
- kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject);
+ kvp.Value.IsStructure
+ ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary())
+ : kvp.Value.AsObject);
}
return expandoObject;
}
- static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue)
+ static Value ConvertJsonObjectToOpenFeatureValue(JsonObject jsonValue)
{
var result = new Dictionary();
- foreach (var property in jsonValue.Properties())
+ foreach (var property in jsonValue.AsEnumerable())
{
- switch (property.Value.Type)
+ switch (property.Value.GetValueKind())
{
- case JTokenType.String:
- result.Add(property.Name, new Value((string)property.Value));
- break;
-
- case JTokenType.Integer:
- result.Add(property.Name, new Value((Int64)property.Value));
+ case JsonValueKind.String:
+ result.Add(property.Key, new Value((string)property.Value));
break;
- case JTokenType.Boolean:
- result.Add(property.Name, new Value((bool)property.Value));
+ case JsonValueKind.Number:
+ result.Add(property.Key, new Value((long)property.Value));
break;
- case JTokenType.Float:
- result.Add(property.Name, new Value((float)property.Value));
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ result.Add(property.Key, new Value((bool)property.Value));
break;
- case JTokenType.Object:
- result.Add(property.Name, ConvertJObjectToOpenFeatureValue((JObject)property.Value));
+ case JsonValueKind.Object:
+ case JsonValueKind.Array:
+ result.Add(property.Key, ConvertJsonObjectToOpenFeatureValue(property.Value.AsObject()));
break;
default:
// Handle unknown data type or throw an exception
- throw new InvalidOperationException($"Unsupported data type: {property.Value.Type}");
+ throw new InvalidOperationException($"Unsupported data type: {property.Value.GetType()}");
}
}
return new Value(new Structure(result));
}
}
-}
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml
deleted file mode 100644
index f6b0c0a2..00000000
--- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-services:
- flagd:
- image: ghcr.io/open-feature/flagd-testbed:v0.5.6
- ports:
- - 8013:8013
- flagd-unstable:
- image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.6
- ports:
- - 8014:8013
- flagd-sync:
- image: ghcr.io/open-feature/sync-testbed:v0.5.6
- ports:
- - 9090:9090
- flagd-sync-unstable:
- image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.6
- ports:
- - 9091:9090
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/schemas b/src/OpenFeature.Contrib.Providers.Flagd/schemas
index 4b44704e..c707f563 160000
--- a/src/OpenFeature.Contrib.Providers.Flagd/schemas
+++ b/src/OpenFeature.Contrib.Providers.Flagd/schemas
@@ -1 +1 @@
-Subproject commit 4b44704e447468fd74d8e95830627ce23b6ef16f
+Subproject commit c707f563d0a1b35ebea802568c6d3151633bde31
diff --git a/src/OpenFeature.Contrib.Providers.Flagd/version.txt b/src/OpenFeature.Contrib.Providers.Flagd/version.txt
index 0d91a54c..9e11b32f 100644
--- a/src/OpenFeature.Contrib.Providers.Flagd/version.txt
+++ b/src/OpenFeature.Contrib.Providers.Flagd/version.txt
@@ -1 +1 @@
-0.3.0
+0.3.1
diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj
index e9453b9b..978c94e5 100644
--- a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj
+++ b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj
@@ -18,11 +18,11 @@
-
+
-
+
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md
new file mode 100644
index 00000000..8756f502
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md
@@ -0,0 +1,29 @@
+# Changelog
+
+## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.4...OpenFeature.Contrib.Providers.Flipt-v0.0.5) (2024-10-18)
+
+
+### 🐛 Bug Fixes
+
+* update readme ([1aaa387](https://github.com/open-feature/dotnet-sdk-contrib/commit/1aaa3877ae3db884d401226b2138f8e3903a56c2))
+
+## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.3...OpenFeature.Contrib.Providers.Flipt-v0.0.4) (2024-10-18)
+
+
+### 🐛 Bug Fixes
+
+* update docs ([#300](https://github.com/open-feature/dotnet-sdk-contrib/issues/300)) ([50fd738](https://github.com/open-feature/dotnet-sdk-contrib/commit/50fd738585567a39f6fd0b1db37b899cbae42ba5))
+
+## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.2...OpenFeature.Contrib.Providers.Flipt-v0.0.3) (2024-10-18)
+
+
+### 🐛 Bug Fixes
+
+* force a republish ([#298](https://github.com/open-feature/dotnet-sdk-contrib/issues/298)) ([ad01db2](https://github.com/open-feature/dotnet-sdk-contrib/commit/ad01db2991a147d527637afac30827f73a4cc40e))
+
+## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.1...OpenFeature.Contrib.Providers.Flipt-v0.0.2) (2024-10-17)
+
+
+### ✨ New Features
+
+* Introduce flipt provider for dotnet ([#293](https://github.com/open-feature/dotnet-sdk-contrib/issues/293)) ([4d59bc3](https://github.com/open-feature/dotnet-sdk-contrib/commit/4d59bc35bd4c65c9989e8c980668d85242240eec))
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs
new file mode 100644
index 00000000..ea332706
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Flipt.Rest;
+
+namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
+
+///
+/// Wrapper for Flipt server sdk client for .net
+///
+public class FliptClientWrapper : IFliptClientWrapper
+{
+ private readonly FliptRestClient _fliptRestClient;
+
+ ///
+ ///
+ /// Url of flipt instance
+ /// Authentication access token
+ /// Timeout when calling flipt endpoints in seconds
+ public FliptClientWrapper(string fliptUrl,
+ string clientToken = "",
+ int timeoutInSeconds = 30)
+ {
+ _fliptRestClient = BuildClient(fliptUrl, clientToken, timeoutInSeconds);
+ }
+
+ ///
+ public async Task EvaluateVariantAsync(EvaluationRequest evaluationRequest)
+ {
+ return await _fliptRestClient.EvaluateV1VariantAsync(evaluationRequest);
+ }
+
+ ///
+ public async Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest)
+ {
+ return await _fliptRestClient.EvaluateV1BooleanAsync(evaluationRequest);
+ }
+
+ private static FliptRestClient BuildClient(string fliptUrl, string clientToken, int timeoutInSeconds = 30)
+ {
+ var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(fliptUrl),
+ Timeout = TimeSpan.FromSeconds(timeoutInSeconds),
+ DefaultRequestHeaders = { { "Authorization", $"Bearer {clientToken}" } }
+ };
+ return new FliptRestClient(httpClient);
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs
new file mode 100644
index 00000000..bd0f0be9
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using Flipt.Rest;
+
+namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
+
+///
+///
+public interface IFliptClientWrapper
+{
+ ///
+ /// Wrapper to Flipt.io/EvaluateVariantAsync method
+ ///
+ ///
+ ///
+ Task EvaluateVariantAsync(EvaluationRequest evaluationRequest);
+
+ ///
+ /// Wrapper to Flipt.io/EvaluateBooleanAsync method
+ ///
+ ///
+ ///
+ Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest);
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs
new file mode 100644
index 00000000..d1ebbbf4
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs
@@ -0,0 +1,23 @@
+using System.Text.Json;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Converters;
+
+///
+/// Extensions for default JsonConverter behavior
+///
+public static class JsonConverterExtensions
+{
+ ///
+ /// JsonConverter serializer settings for Flipt to OpenFeature model deserialization
+ ///
+ public static readonly JsonSerializerOptions DefaultSerializerSettings = new()
+ {
+ WriteIndented = true,
+ AllowTrailingCommas = true,
+ Converters =
+ {
+ new OpenFeatureStructureConverter(),
+ new OpenFeatureValueConverter()
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs
new file mode 100644
index 00000000..96da85b2
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Converters;
+
+///
+/// JsonConverter for OpenFeature Structure type
+///
+public class OpenFeatureStructureConverter : JsonConverter
+{
+ ///
+ public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options)
+ {
+ var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(),
+ JsonConverterExtensions.DefaultSerializerSettings));
+ jsonDoc.WriteTo(writer);
+ }
+
+ ///
+ public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var jsonDocument = JsonDocument.ParseValue(ref reader);
+ var jsonText = jsonDocument.RootElement.GetRawText();
+ return new Structure(JsonSerializer.Deserialize>(jsonText,
+ JsonConverterExtensions.DefaultSerializerSettings));
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs
new file mode 100644
index 00000000..6c638dad
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Converters;
+
+///
+/// OpenFeature Value type converter
+///
+public class OpenFeatureValueConverter : JsonConverter
+{
+ ///
+ public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = new Value();
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.String:
+ return reader.TryGetDateTime(out var dateTimeValue)
+ ? new Value(dateTimeValue)
+ : new Value(reader.GetString() ?? string.Empty);
+ case JsonTokenType.True:
+ case JsonTokenType.False:
+ return new Value(reader.GetBoolean());
+ case JsonTokenType.Number:
+ if (reader.TryGetInt32(out var intValue)) return new Value(intValue);
+ if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue);
+ break;
+ case JsonTokenType.StartArray:
+ return new Value(GenerateValueArray(ref reader, typeToConvert, options));
+ case JsonTokenType.StartObject:
+ return new Value(GetStructure(ref reader, typeToConvert, options));
+ }
+
+ return value;
+ }
+
+ private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var startDepth = reader.CurrentDepth;
+ var structureDictionary = new Dictionary();
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.PropertyName)
+ {
+ var key = reader.GetString();
+ reader.Read();
+ var val = Read(ref reader, typeToConvert, options);
+ structureDictionary[key ?? string.Empty] = val;
+ }
+
+ if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break;
+ }
+
+ return new Structure(structureDictionary);
+ }
+
+
+ private IList GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ var valuesArray = new List();
+ var startDepth = reader.CurrentDepth;
+
+ while (reader.Read())
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.EndArray when reader.CurrentDepth == startDepth:
+ return valuesArray;
+ default:
+ valuesArray.Add(Read(ref reader, typeToConvert, options));
+ break;
+ }
+
+ return valuesArray;
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options)
+ {
+ if (value.IsList)
+ {
+ writer.WriteStartArray();
+ foreach (var val in value.AsList!)
+ {
+ var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject,
+ JsonConverterExtensions.DefaultSerializerSettings));
+ jsonDoc.WriteTo(writer);
+ }
+
+ writer.WriteEndArray();
+ }
+ else
+ {
+ var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject,
+ JsonConverterExtensions.DefaultSerializerSettings));
+ jsonDoc.WriteTo(writer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs
new file mode 100644
index 00000000..9f503533
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using OpenFeature.Contrib.Providers.Flipt.Converters;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.Flipt;
+
+///
+/// Extension helper methods
+///
+public static class FliptExtensions
+{
+ ///
+ /// Transforms openFeature EvaluationContext to a mutable Dictionary that flipt sdk accepts
+ ///
+ /// OpenFeature EvaluationContext
+ ///
+ public static Dictionary ToStringDictionary(this EvaluationContext evaluationContext)
+ {
+ return evaluationContext?.AsDictionary()
+ .ToDictionary(k => k.Key,
+ v => JsonSerializer.Serialize(v.Value.AsObject,
+ JsonConverterExtensions.DefaultSerializerSettings)) ??
+ [];
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs
new file mode 100644
index 00000000..6d0754ef
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs
@@ -0,0 +1,81 @@
+using System.Threading;
+using System.Threading.Tasks;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.Flipt;
+
+///
+/// FliptProvider is the .NET provider implementation for Flipt.io
+///
+///
+/// Accepts an instantiated IFliptClientWrapper instance
+///
+public class FliptProvider : FeatureProvider
+{
+ private static readonly Metadata Metadata = new("Flipt Provider");
+ private readonly IFliptToOpenFeatureConverter _fliptToOpenFeatureConverter;
+
+ ///
+ /// Instantiate a FliptProvider using configuration params
+ ///
+ /// Url of flipt instance
+ /// Namespace used for querying flags
+ /// Authentication access token
+ /// Timeout when calling flipt endpoints in seconds
+ public FliptProvider(string fliptUrl, string namespaceKey = "default", string clientToken = "",
+ int timeoutInSeconds = 30) : this(new FliptToOpenFeatureConverter(fliptUrl, namespaceKey, clientToken,
+ timeoutInSeconds))
+ {
+ }
+
+ internal FliptProvider(IFliptToOpenFeatureConverter fliptToOpenFeatureConverter)
+ {
+ _fliptToOpenFeatureConverter = fliptToOpenFeatureConverter;
+ }
+
+ ///
+ public override Metadata GetMetadata()
+ {
+ return Metadata;
+ }
+
+ ///
+ public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
+ EvaluationContext context = null,
+ CancellationToken cancellationToken = new())
+ {
+ return await _fliptToOpenFeatureConverter.EvaluateBooleanAsync(flagKey, defaultValue, context);
+ }
+
+ ///
+ public override async Task> ResolveStringValueAsync(string flagKey,
+ string defaultValue, EvaluationContext context = null,
+ CancellationToken cancellationToken = new())
+ {
+ return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context);
+ }
+
+ ///
+ public override async Task> ResolveIntegerValueAsync(string flagKey, int defaultValue,
+ EvaluationContext context = null,
+ CancellationToken cancellationToken = new())
+ {
+ return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context);
+ }
+
+ ///
+ public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue,
+ EvaluationContext context = null,
+ CancellationToken cancellationToken = new())
+ {
+ return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context);
+ }
+
+ ///
+ public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue,
+ EvaluationContext context = null,
+ CancellationToken cancellationToken = new())
+ {
+ return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context);
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs
new file mode 100644
index 00000000..cf54c183
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Flipt.Rest;
+using OpenFeature.Constant;
+using OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
+using OpenFeature.Contrib.Providers.Flipt.Converters;
+using OpenFeature.Error;
+using OpenFeature.Model;
+
+namespace OpenFeature.Contrib.Providers.Flipt;
+
+///
+/// A wrapper of fliptClient to handle data casting and error mappings to OpenFeature models
+///
+public class FliptToOpenFeatureConverter : IFliptToOpenFeatureConverter
+{
+ private readonly IFliptClientWrapper _fliptClientWrapper;
+ private readonly string _namespaceKey;
+
+ ///
+ /// Wrapper that uses Flipt to OpenFeature compliant models
+ ///
+ /// Url of flipt instance
+ /// Namespace used for querying flags
+ /// Authentication access token
+ /// Timeout when calling flipt endpoints in seconds
+ public FliptToOpenFeatureConverter(string fliptUrl,
+ string namespaceKey = "default",
+ string clientToken = "",
+ int timeoutInSeconds = 30) : this(new FliptClientWrapper(fliptUrl, clientToken, timeoutInSeconds),
+ namespaceKey)
+ {
+ }
+
+ internal FliptToOpenFeatureConverter(IFliptClientWrapper fliptClientWrapper, string namespaceKey = "default")
+ {
+ _fliptClientWrapper = fliptClientWrapper;
+ _namespaceKey = namespaceKey;
+ }
+
+ ///
+ public async Task> EvaluateAsync(string flagKey, T defaultValue,
+ EvaluationContext context = null)
+ {
+ var evaluationRequest = new EvaluationRequest
+ {
+ NamespaceKey = _namespaceKey,
+ FlagKey = flagKey,
+ EntityId = context?.TargetingKey ?? "",
+ Context = context.ToStringDictionary()
+ };
+
+ try
+ {
+ var evaluationResponse = await _fliptClientWrapper.EvaluateVariantAsync(evaluationRequest);
+
+ if (evaluationResponse.Reason == VariantEvaluationResponseReason.FLAG_DISABLED_EVALUATION_REASON)
+ return new ResolutionDetails(flagKey, defaultValue, ErrorType.None,
+ Reason.Disabled);
+
+ if (!evaluationResponse.Match)
+ return new ResolutionDetails(flagKey, defaultValue, ErrorType.None,
+ Reason.Default);
+ try
+ {
+ if (string.IsNullOrEmpty(evaluationResponse.VariantAttachment))
+ {
+ var convertedValue = (T)Convert.ChangeType(evaluationResponse.VariantKey, typeof(T));
+ return new ResolutionDetails(flagKey,
+ convertedValue, ErrorType.None,
+ Reason.TargetingMatch, evaluationResponse.VariantKey);
+ }
+
+ var deserializedValueObj = JsonSerializer.Deserialize(evaluationResponse.VariantAttachment,
+ JsonConverterExtensions.DefaultSerializerSettings);
+
+ return new ResolutionDetails(flagKey,
+ (T)Convert.ChangeType(deserializedValueObj, typeof(T)),
+ ErrorType.None, Reason.TargetingMatch, evaluationResponse.VariantKey);
+ }
+ catch (Exception ex)
+ {
+ if (ex is InvalidCastException or FormatException)
+ throw new TypeMismatchException(ex.Message, ex);
+ }
+ }
+ catch (FliptRestException ex)
+ {
+ throw HttpRequestExceptionFromFliptRestException(ex);
+ }
+
+ return new ResolutionDetails(flagKey, defaultValue, ErrorType.General, Reason.Unknown);
+ }
+
+ ///
+ public async Task> EvaluateBooleanAsync(string flagKey, bool defaultValue,
+ EvaluationContext context = null)
+ {
+ try
+ {
+ var evaluationRequest = new EvaluationRequest
+ {
+ NamespaceKey = _namespaceKey,
+ FlagKey = flagKey,
+ EntityId = context?.TargetingKey ?? "",
+ Context = context.ToStringDictionary()
+ };
+ var boolEvaluationResponse = await _fliptClientWrapper.EvaluateBooleanAsync(evaluationRequest);
+ return new ResolutionDetails(flagKey, boolEvaluationResponse.Enabled, ErrorType.None,
+ Reason.TargetingMatch);
+ }
+ catch (FliptRestException ex)
+ {
+ throw HttpRequestExceptionFromFliptRestException(ex);
+ }
+ }
+
+ private static Exception HttpRequestExceptionFromFliptRestException(FliptRestException e)
+ {
+ return new HttpRequestException(e.Message, e);
+ }
+}
+
+///
+/// Contract for fliptClient wrapper
+///
+public interface IFliptToOpenFeatureConverter
+{
+ ///
+ /// Used for evaluating non-boolean flags. Flipt handles datatypes which is not boolean as variants
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// OpenFeature ResolutionDetails object
+ Task> EvaluateAsync(string flagKey, T defaultValue, EvaluationContext context = null);
+
+ ///
+ /// Used for evaluating boolean flags
+ ///
+ ///
+ ///
+ ///
+ /// OpenFeature ResolutionDetails object
+ Task> EvaluateBooleanAsync(string flagKey, bool defaultValue,
+ EvaluationContext context = null);
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj
new file mode 100644
index 00000000..ab72f24d
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj
@@ -0,0 +1,44 @@
+
+
+
+ OpenFeature.Contrib.Providers.Flipt
+ 0.0.5
+ $(VersionNumber)
+ $(VersionNumber)
+ $(VersionNumber)
+ Flipt provider for .NET
+ Jean Andrei de la Cruz Austria
+
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Test
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+ latest
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md
new file mode 100644
index 00000000..908769a9
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md
@@ -0,0 +1,134 @@
+# Flipt .NET Provider
+
+The flipt provider allows you to connect to your Flipt instance through the OpenFeature SDK
+
+# .Net SDK usage
+
+## Requirements
+
+- open-feature/dotnet-sdk v1.5.0 > v2.0.0
+
+## Install dependencies
+
+The first thing we will do is install the **OpenFeature SDK** and the **Flipt Feature Flag provider**.
+
+### .NET Cli
+
+```shell
+dotnet add package OpenFeature.Contrib.Providers.Flipt
+```
+
+### Package Manager
+
+```shell
+NuGet\Install-Package OpenFeature.Contrib.Providers.Flipt
+```
+
+### Package Reference
+
+```xml
+
+```
+
+### Packet cli
+
+```shell
+packet add OpenFeature.Contrib.Providers.Flipt
+```
+
+### Cake
+
+```shell
+// Install OpenFeature.Contrib.Providers.Flipt as a Cake Addin
+#addin nuget:?package=OpenFeature.Contrib.Providers.Flipt
+
+// Install OpenFeature.Contrib.Providers.Flipt as a Cake Tool
+#tool nuget:?package=OpenFeature.Contrib.Providers.Flipt
+```
+
+## Using the Flipt Provider with the OpenFeature SDK
+
+To create a Flipt provider, you should define the provider and pass in the instance `url` (required), `defaultNamespace` and
+`token`.
+
+```csharp
+using OpenFeature.Contrib.Providers.Flipt;
+using OpenFeature.Model;
+
+// namespace and clientToken is optional
+var featureProvider = new FliptProvider("http://localhost:8080", "default-namespace", "client-token");
+
+// Set the featureProvider as the provider for the OpenFeature SDK
+await OpenFeature.Api.Instance.SetProviderAsync(featureProvider);
+
+// Get an OpenFeature client
+var client = OpenFeature.Api.Instance.GetClient();
+
+// Optional: set EntityId and updated context
+var context = EvaluationContext.Builder()
+ .SetTargetingKey("flipt EntityId")
+ .Set("extra-data-1", "extra-data-1-value")
+ .Build();
+
+// Evaluate a flag
+var val = await client.GetBooleanValueAsync("myBoolFlag", false, context);
+
+// Print the value of the 'myBoolFlag' feature flag
+Console.WriteLine(val);
+```
+
+# Contribution
+
+## Code setup
+
+Since the official [flipt-csharp](https://github.com/flipt-io/flipt-server-sdks/tree/main/flipt-csharp) only supports
+dotnet 8.0 was not utilized by this provider as OpenFeature aims to support a bigger range of dotnet versions.
+
+### Rest Client using OpenAPI
+
+To work around this incompatibility, the openapi specification
+of [Flipt](https://github.com/flipt-io/flipt/blob/main/openapi.yaml) was
+used to generate a REST client using [nswag](https://github.com/RicoSuter/NSwag).
+
+## Updating the REST Client
+
+To generate or update the Flipt REST client **manually**, follow these steps:
+
+_The **Rest client is generated automatically during build time** using the committed `openapi.yaml` file and is saved
+in the `/obj/` folder_
+
+### 1. Download the OpenAPI Specification
+
+First, download the latest `openapi.yaml` file from the Flipt GitHub repository. This can be done manually or by using a
+command like `curl` in the `/src/OpenFeature.Contrib.Providers.Flipt/`:
+
+```shell
+curl https://raw.githubusercontent.com/flipt-io/flipt/refs/heads/main/openapi.yaml -o openapi.yaml
+```
+
+### 2. Generate the Client Code
+
+With the `openapi.yml` file in your working directory, run the following `nswag` command to generate the REST client
+code. Make sure to correct the command as shown below:
+
+```shell
+nswag openapi2csclient /className:FliptRestClient /namespace:Flipt.Rest /input:"openapi.yaml" /output:"./Flipt.Rest.Client.cs" /GenerateExceptionClasses:true /OperationGenerationMode:SingleClientFromPathSegments /JsonLibrary:SystemTextJson /GenerateOptionalParameters:true /GenerateDefaultValues:true /GenerateResponseClasses:true /GenerateClientInterfaces:true /GenerateClientClasses:true /GenerateDtoTypes:true /ExceptionClass:FliptRestException /GenerateNativeRecords:true /UseBaseUrl:false /GenerateBaseUrlProperty:false
+```
+
+#### Notes
+
+- Ensure the `nswag` CLI tool is correctly installed and accessible from your terminal or command prompt.
+- The command provided generates a C# client for interacting with the Flipt API, leveraging the System.Text.Json library
+ for JSON serialization/deserialization.
+- The generated client will include features such as exception classes, optional parameters, default values, response
+ classes, client interfaces, DTO types, and native records, according to the specified options.
+- This process assumes you're working in a directory that contains the `openapi.yml` file and will generate the
+ `Flipt.Rest.Client.cs` file in the same directory.
+
+## Know issues and limitations
+
+-In `BuildClient()` method
+from https://github.com/open-feature/dotnet-sdk-contrib/blob/204144f6df0dacf46e6d52d34dd6b5a223a853f4/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs#L41-L47
+a new `HttpClient` is created. In the future, it would be better to allow passing of `HttpConnectionFactory` to avoid
+problems regarding socket starvation
+
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml b/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml
new file mode 100644
index 00000000..8e2c006f
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml
@@ -0,0 +1,2310 @@
+# Generated with protoc-gen-openapi
+# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi
+
+openapi: 3.0.3
+info:
+ title: api
+ version: 1.47.0
+servers:
+ - url: http://localhost:8080
+paths:
+ /api/v1/namespaces:
+ get:
+ tags:
+ - Flipt
+ - NamespacesService
+ operationId: listNamespaces
+ parameters:
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NamespaceList'
+ post:
+ tags:
+ - Flipt
+ - NamespacesService
+ operationId: createNamespace
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateNamespaceRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Namespace'
+ /api/v1/namespaces/{key}:
+ get:
+ tags:
+ - Flipt
+ - NamespacesService
+ operationId: getNamespace
+ parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Namespace'
+ put:
+ tags:
+ - Flipt
+ - NamespacesService
+ operationId: updateNamespace
+ parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateNamespaceRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Namespace'
+ delete:
+ tags:
+ - Flipt
+ - NamespacesService
+ operationId: deleteNamespace
+ parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags:
+ get:
+ tags:
+ - Flipt
+ - FlagsService
+ operationId: listFlags
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FlagList'
+ post:
+ tags:
+ - Flipt
+ - FlagsService
+ operationId: createFlag
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateFlagRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Flag'
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts:
+ get:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: listRollouts
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RolloutList'
+ post:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: createRollout
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateRolloutRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rollout'
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts/order:
+ put:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: orderRollouts
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OrderRolloutsRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts/{id}:
+ get:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: getRollout
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rollout'
+ put:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: updateRollout
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateRolloutRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rollout'
+ delete:
+ tags:
+ - Flipt
+ - RolloutsService
+ operationId: deleteRollout
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules:
+ get:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: listRules
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RuleList'
+ post:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: createRule
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateRuleRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rule'
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/order:
+ put:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: orderRules
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OrderRulesRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{id}:
+ get:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: getRule
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rule'
+ put:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: updateRule
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateRuleRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Rule'
+ delete:
+ tags:
+ - Flipt
+ - RulesService
+ operationId: deleteRule
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{ruleId}/distributions:
+ post:
+ tags:
+ - Flipt
+ - DistributionsService
+ operationId: createDistribution
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: ruleId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateDistributionRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Distribution'
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{ruleId}/distributions/{id}:
+ put:
+ tags:
+ - Flipt
+ - DistributionsService
+ operationId: updateDistribution
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: ruleId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateDistributionRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Distribution'
+ delete:
+ tags:
+ - Flipt
+ - DistributionsService
+ operationId: deleteDistribution
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: ruleId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: variantId
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/variants:
+ post:
+ tags:
+ - Flipt
+ - VariantsService
+ operationId: createVariant
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateVariantRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Variant'
+ /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/variants/{id}:
+ put:
+ tags:
+ - Flipt
+ - VariantsService
+ operationId: updateVariant
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateVariantRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Variant'
+ delete:
+ tags:
+ - Flipt
+ - VariantsService
+ operationId: deleteVariant
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: flagKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/flags/{key}:
+ get:
+ tags:
+ - Flipt
+ - FlagsService
+ operationId: getFlag
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Flag'
+ put:
+ tags:
+ - Flipt
+ - FlagsService
+ operationId: updateFlag
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateFlagRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Flag'
+ delete:
+ tags:
+ - Flipt
+ - FlagsService
+ operationId: deleteFlag
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/segments:
+ get:
+ tags:
+ - Flipt
+ - SegmentsService
+ operationId: listSegments
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SegmentList'
+ post:
+ tags:
+ - Flipt
+ - SegmentsService
+ operationId: createSegment
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateSegmentRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Segment'
+ /api/v1/namespaces/{namespaceKey}/segments/{key}:
+ get:
+ tags:
+ - Flipt
+ - SegmentsService
+ operationId: getSegment
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: reference
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Segment'
+ put:
+ tags:
+ - Flipt
+ - SegmentsService
+ operationId: updateSegment
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateSegmentRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Segment'
+ delete:
+ tags:
+ - Flipt
+ - SegmentsService
+ operationId: deleteSegment
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /api/v1/namespaces/{namespaceKey}/segments/{segmentKey}/constraints:
+ post:
+ tags:
+ - Flipt
+ - ConstraintsService
+ operationId: createConstraint
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: segmentKey
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateConstraintRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Constraint'
+ /api/v1/namespaces/{namespaceKey}/segments/{segmentKey}/constraints/{id}:
+ put:
+ tags:
+ - Flipt
+ - ConstraintsService
+ operationId: updateConstraint
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: segmentKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateConstraintRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Constraint'
+ delete:
+ tags:
+ - Flipt
+ - ConstraintsService
+ operationId: deleteConstraint
+ parameters:
+ - name: namespaceKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: segmentKey
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /auth/v1/method/kubernetes/serviceaccount:
+ post:
+ tags:
+ - AuthenticationMethodKubernetesService
+ operationId: kubernetesVerifyServiceAccount
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VerifyServiceAccountRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VerifyServiceAccountResponse'
+ /auth/v1/method/oidc/{provider}/authorize:
+ get:
+ tags:
+ - AuthenticationMethodOIDCService
+ operationId: oidcAuthorizeURL
+ parameters:
+ - name: provider
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: state
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthorizeURLResponse'
+ /auth/v1/method/oidc/{provider}/callback:
+ get:
+ tags:
+ - AuthenticationMethodOIDCService
+ operationId: oidcCallback
+ parameters:
+ - name: provider
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: code
+ in: query
+ schema:
+ type: string
+ - name: state
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CallbackResponse'
+ /auth/v1/method/token:
+ post:
+ tags:
+ - AuthenticationMethodTokenService
+ operationId: createMethodToken
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateTokenRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateTokenResponse'
+ /auth/v1/self:
+ get:
+ tags:
+ - AuthenticationService
+ operationId: getAuthSelf
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Authentication'
+ /auth/v1/self/expire:
+ put:
+ tags:
+ - AuthenticationService
+ operationId: expireAuthSelf
+ parameters:
+ - name: expiresAt
+ in: query
+ schema:
+ type: string
+ format: date-time
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /auth/v1/tokens:
+ get:
+ tags:
+ - AuthenticationService
+ operationId: listAuthTokens
+ parameters:
+ - name: method
+ in: query
+ schema:
+ enum:
+ - METHOD_NONE
+ - METHOD_TOKEN
+ - METHOD_OIDC
+ - METHOD_KUBERNETES
+ - METHOD_GITHUB
+ - METHOD_JWT
+ - METHOD_CLOUD
+ type: string
+ format: enum
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListAuthenticationsResponse'
+ /auth/v1/tokens/{id}:
+ get:
+ tags:
+ - AuthenticationService
+ operationId: getAuthToken
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Authentication'
+ delete:
+ tags:
+ - AuthenticationService
+ operationId: deleteAuthToken
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content: {}
+ /evaluate/v1/batch:
+ post:
+ tags:
+ - EvaluationService
+ operationId: evaluateBatch
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BatchEvaluationRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BatchEvaluationResponse'
+ /evaluate/v1/boolean:
+ post:
+ tags:
+ - EvaluationService
+ operationId: evaluateBoolean
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EvaluationRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BooleanEvaluationResponse'
+ /evaluate/v1/variant:
+ post:
+ tags:
+ - EvaluationService
+ operationId: evaluateVariant
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EvaluationRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VariantEvaluationResponse'
+ /ofrep/v1/configuration:
+ get:
+ tags:
+ - OFREPService
+ description: OFREP provider configuration
+ operationId: ofrep.configuration
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GetProviderConfigurationResponse'
+ /ofrep/v1/evaluate/flags:
+ post:
+ tags:
+ - OFREPService
+ description: OFREP bulk flag evaluation
+ operationId: ofrep.evaluateBulk
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EvaluateBulkRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BulkEvaluationResponse'
+ /ofrep/v1/evaluate/flags/{key}:
+ post:
+ tags:
+ - OFREPService
+ description: OFREP single flag evaluation
+ operationId: ofrep.evaluateFlag
+ parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EvaluateFlagRequest'
+ required: true
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/EvaluatedFlag'
+components:
+ schemas:
+ Authentication:
+ type: object
+ properties:
+ id:
+ type: string
+ method:
+ enum:
+ - METHOD_NONE
+ - METHOD_TOKEN
+ - METHOD_OIDC
+ - METHOD_KUBERNETES
+ - METHOD_GITHUB
+ - METHOD_JWT
+ - METHOD_CLOUD
+ type: string
+ format: enum
+ expiresAt:
+ type: string
+ format: date-time
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ metadata:
+ type: object
+ additionalProperties:
+ type: string
+ AuthorizeURLResponse:
+ type: object
+ properties:
+ authorizeUrl:
+ type: string
+ BatchEvaluationRequest:
+ required:
+ - requests
+ type: object
+ properties:
+ requestId:
+ type: string
+ requests:
+ type: array
+ items:
+ $ref: '#/components/schemas/EvaluationRequest'
+ reference:
+ type: string
+ BatchEvaluationResponse:
+ type: object
+ properties:
+ requestId:
+ type: string
+ responses:
+ type: array
+ items:
+ $ref: '#/components/schemas/EvaluationResponse'
+ requestDurationMillis:
+ type: number
+ format: double
+ BooleanEvaluationResponse:
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ reason:
+ enum:
+ - UNKNOWN_EVALUATION_REASON
+ - FLAG_DISABLED_EVALUATION_REASON
+ - MATCH_EVALUATION_REASON
+ - DEFAULT_EVALUATION_REASON
+ type: string
+ format: enum
+ requestId:
+ type: string
+ requestDurationMillis:
+ type: number
+ format: double
+ timestamp:
+ type: string
+ format: date-time
+ flagKey:
+ type: string
+ BulkEvaluationResponse:
+ required:
+ - flags
+ type: object
+ properties:
+ flags:
+ type: array
+ items:
+ $ref: '#/components/schemas/EvaluatedFlag'
+ CacheInvalidation:
+ type: object
+ properties:
+ polling:
+ $ref: '#/components/schemas/Polling'
+ CallbackResponse:
+ type: object
+ properties:
+ clientToken:
+ type: string
+ authentication:
+ $ref: '#/components/schemas/Authentication'
+ Capabilities:
+ type: object
+ properties:
+ cacheInvalidation:
+ $ref: '#/components/schemas/CacheInvalidation'
+ flagEvaluation:
+ $ref: '#/components/schemas/FlagEvaluation'
+ Constraint:
+ type: object
+ properties:
+ id:
+ type: string
+ segmentKey:
+ type: string
+ type:
+ enum:
+ - UNKNOWN_COMPARISON_TYPE
+ - STRING_COMPARISON_TYPE
+ - NUMBER_COMPARISON_TYPE
+ - BOOLEAN_COMPARISON_TYPE
+ - DATETIME_COMPARISON_TYPE
+ - ENTITY_ID_COMPARISON_TYPE
+ type: string
+ format: enum
+ property:
+ type: string
+ operator:
+ type: string
+ value:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ namespaceKey:
+ type: string
+ description:
+ type: string
+ CreateConstraintRequest:
+ required:
+ - type
+ - property
+ - operator
+ type: object
+ properties:
+ segmentKey:
+ type: string
+ type:
+ enum:
+ - UNKNOWN_COMPARISON_TYPE
+ - STRING_COMPARISON_TYPE
+ - NUMBER_COMPARISON_TYPE
+ - BOOLEAN_COMPARISON_TYPE
+ - DATETIME_COMPARISON_TYPE
+ - ENTITY_ID_COMPARISON_TYPE
+ type: string
+ format: enum
+ property:
+ type: string
+ operator:
+ type: string
+ value:
+ type: string
+ namespaceKey:
+ type: string
+ description:
+ type: string
+ CreateDistributionRequest:
+ required:
+ - variantId
+ - rollout
+ type: object
+ properties:
+ flagKey:
+ type: string
+ ruleId:
+ type: string
+ variantId:
+ type: string
+ rollout:
+ type: number
+ format: float
+ namespaceKey:
+ type: string
+ CreateFlagRequest:
+ required:
+ - key
+ - name
+ - type
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ enabled:
+ type: boolean
+ namespaceKey:
+ type: string
+ type:
+ enum:
+ - VARIANT_FLAG_TYPE
+ - BOOLEAN_FLAG_TYPE
+ type: string
+ format: enum
+ metadata:
+ type: object
+ CreateNamespaceRequest:
+ required:
+ - key
+ - name
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ CreateRolloutRequest:
+ required:
+ - rank
+ type: object
+ properties:
+ namespaceKey:
+ type: string
+ flagKey:
+ type: string
+ rank:
+ type: integer
+ format: int32
+ description:
+ type: string
+ segment:
+ $ref: '#/components/schemas/RolloutSegment'
+ threshold:
+ $ref: '#/components/schemas/RolloutThreshold'
+ CreateRuleRequest:
+ required:
+ - rank
+ type: object
+ properties:
+ flagKey:
+ type: string
+ segmentKey:
+ type: string
+ rank:
+ type: integer
+ format: int32
+ namespaceKey:
+ type: string
+ segmentKeys:
+ type: array
+ items:
+ type: string
+ segmentOperator:
+ enum:
+ - OR_SEGMENT_OPERATOR
+ - AND_SEGMENT_OPERATOR
+ type: string
+ format: enum
+ CreateSegmentRequest:
+ required:
+ - key
+ - name
+ - matchType
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ matchType:
+ enum:
+ - ALL_MATCH_TYPE
+ - ANY_MATCH_TYPE
+ type: string
+ format: enum
+ namespaceKey:
+ type: string
+ CreateTokenRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ description:
+ type: string
+ expiresAt:
+ type: string
+ format: date-time
+ namespaceKey:
+ type: string
+ metadata:
+ type: object
+ additionalProperties:
+ type: string
+ CreateTokenResponse:
+ type: object
+ properties:
+ clientToken:
+ type: string
+ authentication:
+ $ref: '#/components/schemas/Authentication'
+ CreateVariantRequest:
+ required:
+ - key
+ type: object
+ properties:
+ flagKey:
+ type: string
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ attachment:
+ type: string
+ namespaceKey:
+ type: string
+ Distribution:
+ type: object
+ properties:
+ id:
+ type: string
+ ruleId:
+ type: string
+ variantId:
+ type: string
+ rollout:
+ type: number
+ format: float
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ ErrorEvaluationResponse:
+ type: object
+ properties:
+ flagKey:
+ type: string
+ namespaceKey:
+ type: string
+ reason:
+ enum:
+ - UNKNOWN_ERROR_EVALUATION_REASON
+ - NOT_FOUND_ERROR_EVALUATION_REASON
+ type: string
+ format: enum
+ EvaluateBulkRequest:
+ type: object
+ properties:
+ context:
+ type: object
+ additionalProperties:
+ type: string
+ EvaluateFlagRequest:
+ type: object
+ properties:
+ key:
+ type: string
+ context:
+ type: object
+ additionalProperties:
+ type: string
+ EvaluatedFlag:
+ type: object
+ properties:
+ key:
+ type: string
+ reason:
+ enum:
+ - UNKNOWN
+ - DISABLED
+ - TARGETING_MATCH
+ - DEFAULT
+ type: string
+ format: enum
+ variant:
+ type: string
+ metadata:
+ type: object
+ value:
+ $ref: '#/components/schemas/GoogleProtobufValue'
+ EvaluationRequest:
+ required:
+ - namespaceKey
+ - flagKey
+ - entityId
+ - context
+ type: object
+ properties:
+ requestId:
+ type: string
+ namespaceKey:
+ type: string
+ flagKey:
+ type: string
+ entityId:
+ type: string
+ context:
+ type: object
+ additionalProperties:
+ type: string
+ reference:
+ type: string
+ EvaluationResponse:
+ type: object
+ properties:
+ type:
+ enum:
+ - VARIANT_EVALUATION_RESPONSE_TYPE
+ - BOOLEAN_EVALUATION_RESPONSE_TYPE
+ - ERROR_EVALUATION_RESPONSE_TYPE
+ type: string
+ format: enum
+ booleanResponse:
+ $ref: '#/components/schemas/BooleanEvaluationResponse'
+ variantResponse:
+ $ref: '#/components/schemas/VariantEvaluationResponse'
+ errorResponse:
+ $ref: '#/components/schemas/ErrorEvaluationResponse'
+ Flag:
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ enabled:
+ type: boolean
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ variants:
+ type: array
+ items:
+ $ref: '#/components/schemas/Variant'
+ namespaceKey:
+ type: string
+ type:
+ enum:
+ - VARIANT_FLAG_TYPE
+ - BOOLEAN_FLAG_TYPE
+ type: string
+ format: enum
+ defaultVariant:
+ $ref: '#/components/schemas/Variant'
+ metadata:
+ type: object
+ FlagEvaluation:
+ type: object
+ properties:
+ supportedTypes:
+ type: array
+ items:
+ type: string
+ FlagList:
+ type: object
+ properties:
+ flags:
+ type: array
+ items:
+ $ref: '#/components/schemas/Flag'
+ nextPageToken:
+ type: string
+ totalCount:
+ type: integer
+ format: int32
+ GetProviderConfigurationResponse:
+ type: object
+ properties:
+ name:
+ type: string
+ capabilities:
+ $ref: '#/components/schemas/Capabilities'
+ GoogleProtobufValue:
+ description: Represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values.
+ ListAuthenticationsResponse:
+ type: object
+ properties:
+ authentications:
+ type: array
+ items:
+ $ref: '#/components/schemas/Authentication'
+ nextPageToken:
+ type: string
+ Namespace:
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ protected:
+ type: boolean
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ NamespaceList:
+ type: object
+ properties:
+ namespaces:
+ type: array
+ items:
+ $ref: '#/components/schemas/Namespace'
+ nextPageToken:
+ type: string
+ totalCount:
+ type: integer
+ format: int32
+ OrderRolloutsRequest:
+ required:
+ - rolloutIds
+ type: object
+ properties:
+ flagKey:
+ type: string
+ namespaceKey:
+ type: string
+ rolloutIds:
+ type: array
+ items:
+ type: string
+ OrderRulesRequest:
+ required:
+ - ruleIds
+ type: object
+ properties:
+ flagKey:
+ type: string
+ ruleIds:
+ type: array
+ items:
+ type: string
+ namespaceKey:
+ type: string
+ Polling:
+ type: object
+ properties:
+ enabled:
+ type: boolean
+ minPollingIntervalMs:
+ type: integer
+ format: uint32
+ Rollout:
+ type: object
+ properties:
+ id:
+ type: string
+ namespaceKey:
+ type: string
+ flagKey:
+ type: string
+ type:
+ enum:
+ - UNKNOWN_ROLLOUT_TYPE
+ - SEGMENT_ROLLOUT_TYPE
+ - THRESHOLD_ROLLOUT_TYPE
+ type: string
+ format: enum
+ rank:
+ type: integer
+ format: int32
+ description:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ segment:
+ $ref: '#/components/schemas/RolloutSegment'
+ threshold:
+ $ref: '#/components/schemas/RolloutThreshold'
+ RolloutList:
+ type: object
+ properties:
+ rules:
+ type: array
+ items:
+ $ref: '#/components/schemas/Rollout'
+ nextPageToken:
+ type: string
+ totalCount:
+ type: integer
+ format: int32
+ RolloutSegment:
+ type: object
+ properties:
+ segmentKey:
+ type: string
+ value:
+ type: boolean
+ segmentKeys:
+ type: array
+ items:
+ type: string
+ segmentOperator:
+ enum:
+ - OR_SEGMENT_OPERATOR
+ - AND_SEGMENT_OPERATOR
+ type: string
+ format: enum
+ RolloutThreshold:
+ type: object
+ properties:
+ percentage:
+ type: number
+ format: float
+ value:
+ type: boolean
+ Rule:
+ type: object
+ properties:
+ id:
+ type: string
+ flagKey:
+ type: string
+ segmentKey:
+ type: string
+ distributions:
+ type: array
+ items:
+ $ref: '#/components/schemas/Distribution'
+ rank:
+ type: integer
+ format: int32
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ namespaceKey:
+ type: string
+ segmentKeys:
+ type: array
+ items:
+ type: string
+ segmentOperator:
+ enum:
+ - OR_SEGMENT_OPERATOR
+ - AND_SEGMENT_OPERATOR
+ type: string
+ format: enum
+ RuleList:
+ type: object
+ properties:
+ rules:
+ type: array
+ items:
+ $ref: '#/components/schemas/Rule'
+ nextPageToken:
+ type: string
+ totalCount:
+ type: integer
+ format: int32
+ Segment:
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ constraints:
+ type: array
+ items:
+ $ref: '#/components/schemas/Constraint'
+ matchType:
+ enum:
+ - ALL_MATCH_TYPE
+ - ANY_MATCH_TYPE
+ type: string
+ format: enum
+ namespaceKey:
+ type: string
+ SegmentList:
+ type: object
+ properties:
+ segments:
+ type: array
+ items:
+ $ref: '#/components/schemas/Segment'
+ nextPageToken:
+ type: string
+ totalCount:
+ type: integer
+ format: int32
+ UpdateConstraintRequest:
+ required:
+ - type
+ - property
+ - operator
+ type: object
+ properties:
+ id:
+ type: string
+ segmentKey:
+ type: string
+ type:
+ enum:
+ - UNKNOWN_COMPARISON_TYPE
+ - STRING_COMPARISON_TYPE
+ - NUMBER_COMPARISON_TYPE
+ - BOOLEAN_COMPARISON_TYPE
+ - DATETIME_COMPARISON_TYPE
+ - ENTITY_ID_COMPARISON_TYPE
+ type: string
+ format: enum
+ property:
+ type: string
+ operator:
+ type: string
+ value:
+ type: string
+ namespaceKey:
+ type: string
+ description:
+ type: string
+ UpdateDistributionRequest:
+ required:
+ - variantId
+ - rollout
+ type: object
+ properties:
+ id:
+ type: string
+ flagKey:
+ type: string
+ ruleId:
+ type: string
+ variantId:
+ type: string
+ rollout:
+ type: number
+ format: float
+ namespaceKey:
+ type: string
+ UpdateFlagRequest:
+ required:
+ - name
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ enabled:
+ type: boolean
+ namespaceKey:
+ type: string
+ defaultVariantId:
+ type: string
+ metadata:
+ type: object
+ UpdateNamespaceRequest:
+ required:
+ - name
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ UpdateRolloutRequest:
+ type: object
+ properties:
+ id:
+ type: string
+ namespaceKey:
+ type: string
+ flagKey:
+ type: string
+ description:
+ type: string
+ segment:
+ $ref: '#/components/schemas/RolloutSegment'
+ threshold:
+ $ref: '#/components/schemas/RolloutThreshold'
+ UpdateRuleRequest:
+ type: object
+ properties:
+ id:
+ type: string
+ flagKey:
+ type: string
+ segmentKey:
+ type: string
+ namespaceKey:
+ type: string
+ segmentKeys:
+ type: array
+ items:
+ type: string
+ segmentOperator:
+ enum:
+ - OR_SEGMENT_OPERATOR
+ - AND_SEGMENT_OPERATOR
+ type: string
+ format: enum
+ UpdateSegmentRequest:
+ required:
+ - name
+ - matchType
+ type: object
+ properties:
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ matchType:
+ enum:
+ - ALL_MATCH_TYPE
+ - ANY_MATCH_TYPE
+ type: string
+ format: enum
+ namespaceKey:
+ type: string
+ UpdateVariantRequest:
+ required:
+ - key
+ type: object
+ properties:
+ id:
+ type: string
+ flagKey:
+ type: string
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ attachment:
+ type: string
+ namespaceKey:
+ type: string
+ Variant:
+ type: object
+ properties:
+ id:
+ type: string
+ flagKey:
+ type: string
+ key:
+ type: string
+ name:
+ type: string
+ description:
+ type: string
+ createdAt:
+ type: string
+ format: date-time
+ updatedAt:
+ type: string
+ format: date-time
+ attachment:
+ type: string
+ namespaceKey:
+ type: string
+ VariantEvaluationResponse:
+ type: object
+ properties:
+ match:
+ type: boolean
+ segmentKeys:
+ type: array
+ items:
+ type: string
+ reason:
+ enum:
+ - UNKNOWN_EVALUATION_REASON
+ - FLAG_DISABLED_EVALUATION_REASON
+ - MATCH_EVALUATION_REASON
+ - DEFAULT_EVALUATION_REASON
+ type: string
+ format: enum
+ variantKey:
+ type: string
+ variantAttachment:
+ type: string
+ requestId:
+ type: string
+ requestDurationMillis:
+ type: number
+ format: double
+ timestamp:
+ type: string
+ format: date-time
+ flagKey:
+ type: string
+ VerifyServiceAccountRequest:
+ type: object
+ properties:
+ serviceAccountToken:
+ type: string
+ VerifyServiceAccountResponse:
+ type: object
+ properties:
+ clientToken:
+ type: string
+ authentication:
+ $ref: '#/components/schemas/Authentication'
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ jwtAuth:
+ type: http
+ scheme: JWT
+security:
+ - bearerAuth: []
+tags:
+ - name: AuthenticationMethodKubernetesService
+ - name: AuthenticationMethodOIDCService
+ - name: AuthenticationMethodTokenService
+ - name: AuthenticationService
+ - name: EvaluationService
+ - name: Flipt
+ - name: OFREPService
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt
new file mode 100644
index 00000000..bbdeab62
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt
@@ -0,0 +1 @@
+0.0.5
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md
index 85c6b384..7b0e6edc 100644
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md
+++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [0.2.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.0...OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.1) (2025-02-10)
+
+
+### ✨ New Features
+
+* **gofeatureflag:** Provider refactor ([#313](https://github.com/open-feature/dotnet-sdk-contrib/issues/313)) ([c30446e](https://github.com/open-feature/dotnet-sdk-contrib/commit/c30446eb51538b05378db7c4d56228f01ed1cb88))
+
+
+### 🧹 Chore
+
+* **deps:** update dependency system.text.json to 8.0.5 [security] ([#287](https://github.com/open-feature/dotnet-sdk-contrib/issues/287)) ([8cb79ab](https://github.com/open-feature/dotnet-sdk-contrib/commit/8cb79ab8e6d33adc9acb6d6b9795cc4b5e0cf81e))
+
## [0.2.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.GOFeatureFlag-v0.1.10...OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.0) (2024-08-22)
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs
deleted file mode 100644
index b9178d54..00000000
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace OpenFeature.Contrib.Providers.GOFeatureFlag
-{
- ///
- /// GOFeatureFlagRequest is the object formatting the request to the relay proxy.
- ///
- /// Type of the default value.
- public class GOFeatureFlagRequest
- {
- ///
- /// GoFeatureFlagUser is the representation of the user.
- ///
- public GoFeatureFlagUser User { get; set; }
-
- ///
- /// default value if we have an error.
- ///
- public T DefaultValue { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs
index 82b7994d..fddca3ec 100644
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs
+++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.Net;
using System.Net.Http;
@@ -9,7 +10,11 @@
using System.Threading;
using System.Threading.Tasks;
using OpenFeature.Constant;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.converters;
using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.extensions;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.hooks;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
using OpenFeature.Model;
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
@@ -20,8 +25,8 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag
public class GoFeatureFlagProvider : FeatureProvider
{
private const string ApplicationJson = "application/json";
+ private ExporterMetadata _exporterMetadata;
private HttpClient _httpClient;
- private JsonSerializerOptions _serializerOptions;
///
/// Constructor of the provider.
@@ -34,6 +39,17 @@ public GoFeatureFlagProvider(GoFeatureFlagProviderOptions options)
InitializeProvider(options);
}
+ ///
+ /// List of hooks to use for this provider
+ ///
+ ///
+ public override IImmutableList GetProviderHooks()
+ {
+ var hooks = ImmutableArray.CreateBuilder();
+ hooks.Add(new EnrichEvaluationContextHook(_exporterMetadata));
+ return hooks.ToImmutable();
+ }
+
///
/// validateInputOptions is validating the different options provided when creating the provider.
///
@@ -53,6 +69,10 @@ private void ValidateInputOptions(GoFeatureFlagProviderOptions options)
/// Options used while creating the provider
private void InitializeProvider(GoFeatureFlagProviderOptions options)
{
+ _exporterMetadata = options.ExporterMetadata ?? new ExporterMetadata();
+ _exporterMetadata.Add("provider", ".NET");
+ _exporterMetadata.Add("openfeature", true);
+
_httpClient = options.HttpMessageHandler != null
? new HttpClient(options.HttpMessageHandler)
: new HttpClient
@@ -63,7 +83,6 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options)
};
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
_httpClient.BaseAddress = new Uri(options.Endpoint);
- _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
if (options.ApiKey != null)
_httpClient.DefaultRequestHeaders.Authorization =
@@ -96,8 +115,8 @@ public override async Task> ResolveBooleanValueAsync(str
try
{
var resp = await CallApi(flagKey, defaultValue, context);
- return new ResolutionDetails(flagKey, bool.Parse(resp.value.ToString()), ErrorType.None,
- resp.reason, resp.variationType);
+ return new ResolutionDetails(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None,
+ resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
@@ -121,16 +140,17 @@ public override async Task> ResolveBooleanValueAsync(str
/// If the flag does not exists
/// If an unknown error happen
/// If the flag is disabled
- public override async Task> ResolveStringValueAsync(string flagKey, string defaultValue,
+ public override async Task> ResolveStringValueAsync(string flagKey,
+ string defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
- if (!(resp.value is JsonElement element && element.ValueKind == JsonValueKind.String))
+ if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String))
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
- return new ResolutionDetails(flagKey, resp.value.ToString(), ErrorType.None, resp.reason,
- resp.variationType);
+ return new ResolutionDetails(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason,
+ resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
@@ -160,8 +180,8 @@ public override async Task> ResolveIntegerValueAsync(stri
try
{
var resp = await CallApi(flagKey, defaultValue, context);
- return new ResolutionDetails(flagKey, int.Parse(resp.value.ToString()), ErrorType.None,
- resp.reason, resp.variationType);
+ return new ResolutionDetails(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None,
+ resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
@@ -185,15 +205,16 @@ public override async Task> ResolveIntegerValueAsync(stri
/// If the flag does not exists
/// If an unknown error happen
/// If the flag is disabled
- public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue,
+ public override async Task> ResolveDoubleValueAsync(string flagKey,
+ double defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
return new ResolutionDetails(flagKey,
- double.Parse(resp.value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
- resp.reason, resp.variationType);
+ double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
+ resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
@@ -217,17 +238,18 @@ public override async Task> ResolveDoubleValueAsync(st
/// If the flag does not exists
/// If an unknown error happen
/// If the flag is disabled
- public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue,
+ public override async Task> ResolveStructureValueAsync(string flagKey,
+ Value defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
- if (resp.value is JsonElement)
+ if (resp.Value is JsonElement)
{
- var value = ConvertValue((JsonElement)resp.value);
- return new ResolutionDetails(flagKey, value, ErrorType.None, resp.reason,
- resp.variationType);
+ var value = ConvertValue((JsonElement)resp.Value);
+ return new ResolutionDetails(flagKey, value, ErrorType.None, resp.Reason,
+ resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
@@ -253,39 +275,40 @@ public override async Task> ResolveStructureValueAsync(
/// If the flag does not exists
/// If an unknown error happen
/// If the flag is disabled
- private async Task CallApi(string flagKey, T defaultValue,
+ private async Task CallApi(string flagKey, T defaultValue,
EvaluationContext context = null)
{
- var request = new GOFeatureFlagRequest
- {
- User = context,
- DefaultValue = defaultValue
- };
- var goffRequest = JsonSerializer.Serialize(request, _serializerOptions);
-
- var response = await _httpClient.PostAsync($"v1/feature/{flagKey}/eval",
- new StringContent(goffRequest, Encoding.UTF8, ApplicationJson));
+ var request = new OfrepRequest(context);
+ var response = await _httpClient.PostAsync($"ofrep/v1/evaluate/flags/{flagKey}",
+ new StringContent(request.AsJsonString(), Encoding.UTF8, ApplicationJson));
if (response.StatusCode == HttpStatusCode.NotFound)
throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration");
- if (response.StatusCode == HttpStatusCode.Unauthorized)
+ if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
throw new UnauthorizedError("invalid token used to contact GO Feature Flag relay proxy instance");
if (response.StatusCode >= HttpStatusCode.BadRequest)
throw new GeneralError("impossible to contact GO Feature Flag relay proxy instance");
var responseBody = await response.Content.ReadAsStringAsync();
- var goffResp =
- JsonSerializer.Deserialize(responseBody);
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ var ofrepResp =
+ JsonSerializer.Deserialize(responseBody, options);
- if (goffResp != null && Reason.Disabled.Equals(goffResp.reason))
+ if (Reason.Disabled.Equals(ofrepResp?.Reason))
throw new FlagDisabled();
- if ("FLAG_NOT_FOUND".Equals(goffResp.errorCode))
+ if ("FLAG_NOT_FOUND".Equals(ofrepResp?.ErrorCode))
throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration");
- return goffResp;
+ if (ofrepResp?.Metadata != null)
+ ofrepResp.Metadata = DictionaryConverter.ConvertDictionary(ofrepResp.Metadata);
+
+ return ofrepResp;
}
///
@@ -337,4 +360,4 @@ private Value ConvertValue(JsonElement value)
throw new ImpossibleToConvertTypeError($"impossible to convert the object {value}");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs
index e1e3b20a..3d3100cc 100644
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs
+++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs
@@ -1,5 +1,6 @@
using System;
using System.Net.Http;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
{
@@ -34,5 +35,11 @@ public class GoFeatureFlagProviderOptions
/// Default: null
///
public string ApiKey { get; set; }
+
+ ///
+ /// (optional) ExporterMetadata are static information you can set that will be available in the
+ /// evaluation data sent to the exporter.
+ ///
+ public ExporterMetadata ExporterMetadata { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs
deleted file mode 100644
index c8de3dd6..00000000
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-namespace OpenFeature.Contrib.Providers.GOFeatureFlag
-{
- ///
- /// GoFeatureFlagResponse is the response returned by the relay proxy.
- ///
- public class GoFeatureFlagResponse
- {
- ///
- /// trackEvent is true when this call was tracked in GO Feature Flag.
- ///
- public bool trackEvents { get; set; }
-
- ///
- /// variationType contains the name of the variation used for this flag.
- ///
- public string variationType { get; set; }
-
- ///
- /// failed is true if GO Feature Flag had an issue.
- ///
- public bool failed { get; set; }
-
- ///
- /// version of the flag used (optional)
- ///
- public string version { get; set; }
-
- ///
- /// reason used to choose this variation.
- ///
- public string reason { get; set; }
-
- ///
- /// errorCode is empty if everything went ok.
- ///
- public string errorCode { get; set; }
-
- ///
- /// value contains the result of the flag.
- ///
- public object value { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs
deleted file mode 100644
index 65a013a8..00000000
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
-using OpenFeature.Model;
-
-namespace OpenFeature.Contrib.Providers.GOFeatureFlag
-{
- ///
- /// GOFeatureFlagUser is the representation of a User inside GO Feature Flag.
- ///
- public class GoFeatureFlagUser
- {
- private const string AnonymousField = "anonymous";
- private const string KeyField = "targetingKey";
-
- ///
- /// The targeting key for the user.
- ///
- public string Key { get; private set; }
-
- ///
- /// Is the user Anonymous.
- ///
- public bool Anonymous { get; private set; }
-
- ///
- /// Additional Custom Data to pass to GO Feature Flag.
- ///
- public Dictionary Custom { get; private set; }
-
- /**
- * Convert the evaluation context into a GOFeatureFlagUser Object.
- */
- public static implicit operator GoFeatureFlagUser(EvaluationContext ctx)
- {
- try
- {
- if (ctx is null)
- throw new InvalidEvaluationContext("GO Feature Flag need an Evaluation context to work.");
- if (!ctx.GetValue(KeyField).IsString)
- throw new InvalidTargetingKey("targetingKey field MUST be a string.");
- }
- catch (KeyNotFoundException e)
- {
- throw new InvalidTargetingKey("targetingKey field is mandatory.", e);
- }
-
- var anonymous = ctx.ContainsKey(AnonymousField) && ctx.GetValue(AnonymousField).IsBoolean
- ? ctx.GetValue(AnonymousField).AsBoolean
- : false;
-
- var custom = ctx.AsDictionary().ToDictionary(x => x.Key, x => x.Value.AsObject);
- custom.Remove(AnonymousField);
- custom.Remove(KeyField);
-
- return new GoFeatureFlagUser
- {
- Key = ctx.GetValue("targetingKey").AsString,
- Anonymous = anonymous.Value,
- Custom = custom
- };
- }
- }
-}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj
index 5d4319a3..3dc488c8 100644
--- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj
+++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj
@@ -2,7 +2,7 @@
OpenFeature.Contrib.GOFeatureFlag
- 0.2.0
+ 0.2.1
$(VersionNumber)
$(VersionNumber)
$(VersionNumber)
@@ -11,7 +11,7 @@
-
+
diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs
new file mode 100644
index 00000000..e7f9f319
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+
+namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters
+{
+ ///
+ /// DictionaryConverter is converting a json Dictionary to a Dictionary with real object.
+ ///
+ public static class DictionaryConverter
+ {
+ ///
+ /// Function that convert the dictionary to a Dictionary with real object.
+ ///
+ ///
+ /// A dictionary with real types.
+ public static Dictionary ConvertDictionary(Dictionary inputDictionary)
+ {
+ return inputDictionary.ToDictionary(
+ kvp => kvp.Key,
+ kvp => ConvertValue(kvp.Value)
+ );
+ }
+
+ ///
+ /// Function that convert a value to a object.
+ ///
+ ///
+ /// A value with real types.
+ public static object ConvertValue(object value)
+ {
+ if (value is JsonElement jsonElement)
+ switch (jsonElement.ValueKind)
+ {
+ case JsonValueKind.String:
+ return jsonElement.GetString();
+ case JsonValueKind.Number:
+ if (jsonElement.TryGetInt32(out var intValue)) return intValue;
+
+ if (jsonElement.TryGetDouble(out var doubleValue)) return doubleValue;
+ return jsonElement.GetRawText(); // Fallback to string if not int or double
+ case JsonValueKind.True:
+ return true;
+ case JsonValueKind.False:
+ return false;
+ case JsonValueKind.Null:
+ return null;
+ case JsonValueKind.Object:
+ return ConvertDictionary(
+ JsonSerializer
+ .Deserialize>(jsonElement
+ .GetRawText())); //Recursive for nested objects
+ case JsonValueKind.Array:
+ var array = new List
-
-
+
+
diff --git a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json
index 778394e6..d21b7915 100644
--- a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json
+++ b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json
@@ -1,248 +1,210 @@
{
- "FeatureManagement": {
- "Flag_Boolean_AlwaysOn": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": true
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": false
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ "feature_management": {
+ "feature_flags": [
+ {
+ "id": "Flag_Boolean_AlwaysOn",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": true
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": false
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ]
- },
- "Flag_Boolean_AlwaysOff": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
},
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": true
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": false
+ {
+ "id": "Flag_Boolean_AlwaysOff",
+ "enabled": false,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": true
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": false
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": []
- },
-
- "Flag_Double_AlwaysOn": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1.0
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1.0
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+
+ {
+ "id": "Flag_Double_AlwaysOn",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1.0
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1.0
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ]
- },
- "Flag_Double_AlwaysOff": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1.0
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1.0
+ {
+ "id": "Flag_Double_AlwaysOff",
+ "enabled": false,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1.0
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1.0
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": []
- },
-
- "Flag_Integer_AlwaysOn": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
},
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+
+ {
+ "id": "Flag_Integer_AlwaysOn",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ]
- },
- "Flag_Integer_AlwaysOff": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
},
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1
+ {
+ "id": "Flag_Integer_AlwaysOff",
+ "enabled": false,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": []
- },
-
- "Flag_String_AlwaysOn": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
},
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": "FlagEnabled"
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": "FlagDisabled"
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+
+ {
+ "id": "Flag_String_AlwaysOn",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": "FlagEnabled"
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": "FlagDisabled"
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ]
- },
- "Flag_String_AlwaysOff": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
},
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": "FlagEnabled"
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": "FlagDisabled"
+ {
+ "id": "Flag_String_AlwaysOff",
+ "enabled": false,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": "FlagEnabled"
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": "FlagDisabled"
+ }
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": []
- },
-
- "Flag_Structure_AlwaysOn": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOn",
- "SubStructure": {
- "Field2": 1
+
+ {
+ "id": "Flag_Structure_AlwaysOn",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOn",
+ "SubStructure": {
+ "Field2": 1
+ }
}
- }
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOff",
- "SubStructure": {
- "Field2": -1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOff",
+ "SubStructure": {
+ "Field2": -1
+ }
}
}
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
- }
- ]
- },
- "Flag_Structure_AlwaysOff": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled"
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOn",
- "SubStructure": {
- "Field2": 1
+ {
+ "id": "Flag_Structure_AlwaysOff",
+ "enabled": false,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOn",
+ "SubStructure": {
+ "Field2": 1
+ }
}
- }
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOff",
- "SubStructure": {
- "Field2": -1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOff",
+ "SubStructure": {
+ "Field2": -1
+ }
}
}
+ ],
+ "allocation": {
+ "default_when_enabled": "FlagEnabled",
+ "default_when_disabled": "FlagDisabled"
}
- ],
- "EnabledFor": []
- }
+ }
+ ]
}
}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json
index 2c75b500..7a4d3873 100644
--- a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json
+++ b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json
@@ -1,348 +1,286 @@
{
- "FeatureManagement": {
- "Flag_Boolean_TargetingUserId": {
- "Telemetry": {
- "Enabled": true
- },
- "Allocation": {
- "DefaultWhenDisabled": "FlagDisabled",
- "DefaultWhenEnabled": "FlagDisabled",
- "User": [
+ "feature_management": {
+ "feature_flags": [
+ {
+ "id": "Flag_Boolean_TargetingUserId",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": true
+ },
{
- "Variant": "FlagEnabled",
- "Users": [
- "test.user@openfeature.dev"
- ]
+ "name": "FlagDisabled",
+ "configuration_value": false
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": true
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": false
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "user": [
+ {
+ "variant": "FlagEnabled",
+ "users": [
+ "test.user@openfeature.dev"
+ ]
+ }
+ ]
}
- ]
- },
- "Flag_Boolean_TargetingGroup": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "Group": [
+ {
+ "id": "Flag_Boolean_TargetingGroup",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": true
+ },
{
- "Variant": "FlagEnabled",
- "Groups": [
- "test.group"
- ]
+ "name": "FlagDisabled",
+ "configuration_value": false
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": true
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": false
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "group": [
+ {
+ "variant": "FlagEnabled",
+ "groups": [
+ "test.group"
+ ]
+ }
+ ]
}
- ]
- },
-
- "Flag_Double_TargetingUserId": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "User": [
+ {
+ "id": "Flag_Double_TargetingUserId",
+ "enabled": true,
+ "variants": [
{
- "Variant": "FlagEnabled",
- "Users": [
- "test.user@openfeature.dev"
- ]
+ "name": "FlagEnabled",
+ "configuration_value": 1.0
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1.0
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1.0
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1.0
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "user": [
+ {
+ "variant": "FlagEnabled",
+ "users": [
+ "test.user@openfeature.dev"
+ ]
+ }
+ ]
}
- ]
- },
- "Flag_Double_TargetingGroup": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "Group": [
+ {
+ "id": "Flag_Double_TargetingGroup",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1.0
+ },
{
- "Variant": "FlagEnabled",
- "Groups": [
- "test.group"
- ]
+ "name": "FlagDisabled",
+ "configuration_value": -1.0
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1.0
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1.0
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "group": [
+ {
+ "variant": "FlagEnabled",
+ "groups": [
+ "test.group"
+ ]
+ }
+ ]
}
- ]
- },
-
- "Flag_Integer_TargetingUserId": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "User": [
+ {
+ "id": "Flag_Integer_TargetingUserId",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": 1
+ },
{
- "Variant": "FlagEnabled",
- "Users": [
- "test.user@openfeature.dev"
- ]
+ "name": "FlagDisabled",
+ "configuration_value": -1
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "user": [
+ {
+ "variant": "FlagEnabled",
+ "users": [
+ "test.user@openfeature.dev"
+ ]
+ }
+ ]
}
- ]
- },
- "Flag_Integer_TargetingGroup": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "Group": [
+ {
+ "id": "Flag_Integer_TargetingGroup",
+ "enabled": true,
+ "variants": [
{
- "Variant": "FlagEnabled",
- "Groups": [
- "test.group"
- ]
+ "name": "FlagEnabled",
+ "configuration_value": 1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": -1
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": 1
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": -1
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "group": [
+ {
+ "variant": "FlagEnabled",
+ "groups": [
+ "test.group"
+ ]
+ }
+ ]
}
- ]
- },
-
- "Flag_String_TargetingUserId": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "User": [
+ {
+ "id": "Flag_String_TargetingUserId",
+ "enabled": true,
+ "variants": [
+ {
+ "name": "FlagEnabled",
+ "configuration_value": "FlagEnabled"
+ },
{
- "Variant": "FlagEnabled",
- "Users": [
- "test.user@openfeature.dev"
- ]
+ "name": "FlagDisabled",
+ "configuration_value": "FlagDisabled"
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": "FlagEnabled"
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": "FlagDisabled"
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "user": [
+ {
+ "variant": "FlagEnabled",
+ "users": [
+ "test.user@openfeature.dev"
+ ]
+ }
+ ]
}
- ]
- },
- "Flag_String_TargetingGroup": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "Group": [
+ {
+ "id": "Flag_String_TargetingGroup",
+ "enabled": true,
+ "variants": [
{
- "Variant": "FlagEnabled",
- "Groups": [
- "test.group"
- ]
+ "name": "FlagEnabled",
+ "configuration_value": "FlagEnabled"
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": "FlagDisabled"
}
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": "FlagEnabled"
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": "FlagDisabled"
- }
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "group": [
+ {
+ "variant": "FlagEnabled",
+ "groups": [
+ "test.group"
+ ]
+ }
+ ]
}
- ]
- },
-
- "Flag_Structure_TargetingUserId": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagEnabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "User": [
+ {
+ "id": "Flag_Structure_TargetingUserId",
+ "enabled": true,
+ "variants": [
{
- "Variant": "FlagEnabled",
- "Users": [
- "test.user@openfeature.dev"
- ]
- }
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOn",
- "SubStructure": {
- "Field2": 1
+ "name": "FlagEnabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOn",
+ "SubStructure": {
+ "Field2": 1
+ }
}
- }
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOff",
- "SubStructure": {
- "Field2": -1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOff",
+ "SubStructure": {
+ "Field2": -1
+ }
}
}
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "user": [
+ {
+ "variant": "FlagEnabled",
+ "users": [
+ "test.user@openfeature.dev"
+ ]
+ }
+ ]
}
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
- }
- ]
- },
- "Flag_Structure_TargetingGroup": {
- "Telemetry": {
- "Enabled": true
},
- "Allocation": {
- "DefaultWhenEnabled": "FlagDisabled",
- "DefaultWhenDisabled": "FlagDisabled",
- "Group": [
+ {
+ "id": "Flag_Structure_TargetingGroup",
+ "enabled": true,
+ "variants": [
{
- "Variant": "FlagEnabled",
- "Groups": [
- "test.group"
- ]
- }
- ]
- },
- "Variants": [
- {
- "Name": "FlagEnabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOn",
- "SubStructure": {
- "Field2": 1
+ "name": "FlagEnabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOn",
+ "SubStructure": {
+ "Field2": 1
+ }
}
- }
- },
- {
- "Name": "FlagDisabled",
- "ConfigurationValue": {
- "Field1": "Field1ValueOff",
- "SubStructure": {
- "Field2": -1
+ },
+ {
+ "name": "FlagDisabled",
+ "configuration_value": {
+ "Field1": "Field1ValueOff",
+ "SubStructure": {
+ "Field2": -1
+ }
}
}
+ ],
+ "allocation": {
+ "default_when_disabled": "FlagDisabled",
+ "default_when_enabled": "FlagDisabled",
+ "group": [
+ {
+ "variant": "FlagEnabled",
+ "groups": [
+ "test.group"
+ ]
+ }
+ ]
}
- ],
- "EnabledFor": [
- {
- "Name": "AlwaysOn"
- }
- ]
- }
+ }
+ ]
}
}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs
new file mode 100644
index 00000000..d5a7be03
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs
@@ -0,0 +1,18 @@
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest
+{
+ public class FlagdSyncTestBedContainer
+ {
+ public IContainer Container { get; }
+
+ public FlagdSyncTestBedContainer()
+ {
+ Container = new ContainerBuilder()
+ .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21")
+ .WithPortBinding(8015, true)
+ .Build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj
index a7d4c7f6..67d45846 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj
@@ -27,6 +27,7 @@
+
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs
index a9ea5f53..6863aef4 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs
@@ -1,16 +1,27 @@
-using System;
+using OpenFeature.Contrib.Providers.Flagd.E2e.Test;
using TechTalk.SpecFlow;
-namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps
{
[Binding, Scope(Feature = "Flag evaluation")]
public class EvaluationStepDefinitionsProcess : EvaluationStepDefinitionsBase
{
static EvaluationStepDefinitionsProcess()
{
- var flagdProvider = new FlagdProvider(FlagdConfig.Builder().WithPort(9090).WithResolverType(ResolverType.IN_PROCESS).Build());
+ var host = TestHooks.FlagdSyncTestBed.Container.Hostname;
+ var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015);
+
+ var flagdProvider = new FlagdProvider(
+ FlagdConfig.Builder()
+ .WithHost(host)
+ .WithPort(port)
+ .WithResolverType(ResolverType.IN_PROCESS)
+ .Build()
+ );
+
Api.Instance.SetProviderAsync("process-test-evaluation", flagdProvider).Wait(5000);
}
+
public EvaluationStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext)
{
client = Api.Instance.GetClient("process-test-evaluation");
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs
index 0392c43c..7dc81276 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs
@@ -1,7 +1,7 @@
-using System;
+using OpenFeature.Contrib.Providers.Flagd.E2e.Test;
using TechTalk.SpecFlow;
-namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps
{
[Binding]
[Scope(Feature = "flagd providers")]
@@ -10,9 +10,20 @@ public class FlagdStepDefinitionsProcess : FlagdStepDefinitionsBase
{
static FlagdStepDefinitionsProcess()
{
- var flagdProvider = new FlagdProvider(FlagdConfig.Builder().WithPort(9090).WithResolverType(ResolverType.IN_PROCESS).Build());
+ var host = TestHooks.FlagdSyncTestBed.Container.Hostname;
+ var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015);
+
+ var flagdProvider = new FlagdProvider(
+ FlagdConfig.Builder()
+ .WithHost(host)
+ .WithPort(port)
+ .WithResolverType(ResolverType.IN_PROCESS)
+ .Build()
+ );
+
Api.Instance.SetProviderAsync("process-test-flagd", flagdProvider).Wait(5000);
}
+
public FlagdStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext)
{
client = Api.Instance.GetClient("process-test-flagd");
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs
new file mode 100644
index 00000000..ac7b7503
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs
@@ -0,0 +1,29 @@
+using System.Threading.Tasks;
+using TechTalk.SpecFlow;
+
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps
+{
+ [Binding]
+ public class TestHooks
+ {
+ public static FlagdSyncTestBedContainer FlagdSyncTestBed { get; private set; }
+
+ [BeforeTestRun]
+ public static async Task StartContainerAsync()
+ {
+ FlagdSyncTestBed = new FlagdSyncTestBedContainer();
+
+ await FlagdSyncTestBed.Container.StartAsync();
+ }
+
+ [AfterTestRun]
+ public static async Task StopContainerAsync()
+ {
+ if (FlagdSyncTestBed != null)
+ {
+ await FlagdSyncTestBed.Container.StopAsync();
+ await FlagdSyncTestBed.Container.DisposeAsync();
+ }
+ }
+ }
+}
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs
new file mode 100644
index 00000000..7f3e3565
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs
@@ -0,0 +1,18 @@
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest
+{
+ public class FlagdRpcTestBedContainer
+ {
+ public IContainer Container { get; }
+
+ public FlagdRpcTestBedContainer()
+ {
+ Container = new ContainerBuilder()
+ .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21")
+ .WithPortBinding(8013, true)
+ .Build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj
index a7d4c7f6..67d45846 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj
@@ -27,6 +27,7 @@
+
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs
index e36e96b0..0e7e5e62 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs
@@ -1,17 +1,26 @@
-
+using OpenFeature.Contrib.Providers.Flagd.E2e.Test;
using TechTalk.SpecFlow;
-
-namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps
{
[Binding, Scope(Feature = "Flag evaluation")]
public class EvaluationStepDefinitionsRpc : EvaluationStepDefinitionsBase
{
static EvaluationStepDefinitionsRpc()
{
- var flagdProvider = new FlagdProvider();
+ var host = TestHooks.FlagdTestBed.Container.Hostname;
+ var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013);
+
+ var flagdProvider = new FlagdProvider(
+ FlagdConfig.Builder()
+ .WithHost(host)
+ .WithPort(port)
+ .Build()
+ );
+
Api.Instance.SetProviderAsync("rpc-test-evaluation", flagdProvider).Wait(5000);
}
+
public EvaluationStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext)
{
client = Api.Instance.GetClient("rpc-test-evaluation");
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs
index a1cf9b68..831221ce 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs
@@ -1,20 +1,28 @@
-
+using OpenFeature.Contrib.Providers.Flagd.E2e.Test;
using TechTalk.SpecFlow;
-
-namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps
{
[Binding]
[Scope(Feature = "flagd providers")]
[Scope(Feature = "flagd json evaluation")]
public class FlagdStepDefinitionsRpc : FlagdStepDefinitionsBase
{
-
static FlagdStepDefinitionsRpc()
{
- var flagdProvider = new FlagdProvider();
+ var host = TestHooks.FlagdTestBed.Container.Hostname;
+ var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013);
+
+ var flagdProvider = new FlagdProvider(
+ FlagdConfig.Builder()
+ .WithHost(host)
+ .WithPort(port)
+ .Build()
+ );
+
Api.Instance.SetProviderAsync("rpc-test-flagd", flagdProvider).Wait(5000);
}
+
public FlagdStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext)
{
client = Api.Instance.GetClient("rpc-test-flagd");
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs
new file mode 100644
index 00000000..ed1a3a99
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs
@@ -0,0 +1,29 @@
+using System.Threading.Tasks;
+using TechTalk.SpecFlow;
+
+namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps
+{
+ [Binding]
+ public class TestHooks
+ {
+ public static FlagdRpcTestBedContainer FlagdTestBed { get; private set; }
+
+ [BeforeTestRun]
+ public static async Task StartContainerAsync()
+ {
+ FlagdTestBed = new FlagdRpcTestBedContainer();
+
+ await FlagdTestBed.Container.StartAsync();
+ }
+
+ [AfterTestRun]
+ public static async Task StopContainerAsync()
+ {
+ if (FlagdTestBed != null)
+ {
+ await FlagdTestBed.Container.StopAsync();
+ await FlagdTestBed.Container.DisposeAsync();
+ }
+ }
+ }
+}
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs
index cd23b3ca..72fd85de 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs
@@ -1,6 +1,8 @@
using System.Collections.Generic;
-using JsonLogic.Net;
-using Newtonsoft.Json.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
+using Json.More;
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
using Xunit;
@@ -31,9 +33,7 @@ public class FractionalEvaluatorTest
public void Evaluate(string email, string flagKey, string expected)
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var fractionalEvaluator = new FractionalEvaluator();
- EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate);
+ RuleRegistry.AddRule("fractional", new FractionalEvaluator());
var targetingString = @"{""fractional"": [
{
@@ -42,19 +42,21 @@ public void Evaluate(string email, string flagKey, string expected)
{ ""var"":""email"" }
]
},
- [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25],
+ [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25]
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary {
- { "email", email },
- {"$flagd", new Dictionary { {"flagKey", flagKey } } }
- };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "email", email },
+ { "$flagd", new Dictionary { { "flagKey", flagKey } } }
+ }));
+
+ // Act
+ var result = JsonLogic.Apply(rule, data);
- // Act & Assert
- var result = evaluator.Apply(rule, data);
+ // Assert
Assert.Equal(expected, result.ToString());
}
@@ -63,9 +65,7 @@ public void Evaluate(string email, string flagKey, string expected)
public void EvaluateUsingRelativeWeights(string email, string flagKey, string expected)
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var fractionalEvaluator = new FractionalEvaluator();
- EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate);
+ RuleRegistry.AddRule("fractional", new FractionalEvaluator());
var targetingString = @"{""fractional"": [
{
@@ -74,19 +74,21 @@ public void EvaluateUsingRelativeWeights(string email, string flagKey, string ex
{ ""var"":""email"" }
]
},
- [""red"", 5], [""blue"", 5], [""green"", 5], [""yellow"", 5],
+ [""red"", 5], [""blue"", 5], [""green"", 5], [""yellow"", 5]
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary {
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
{ "email", email },
- {"$flagd", new Dictionary { {"flagKey", flagKey } } }
- };
+ { "$flagd", new Dictionary { { "flagKey", flagKey } } }
+ }));
+
+ // Act
+ var result = JsonLogic.Apply(rule, data);
- // Act & Assert
- var result = evaluator.Apply(rule, data);
+ // Assert
Assert.Equal(expected, result.ToString());
}
@@ -95,9 +97,7 @@ public void EvaluateUsingRelativeWeights(string email, string flagKey, string ex
public void EvaluateUsingDefaultWeights(string email, string flagKey, string expected)
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var fractionalEvaluator = new FractionalEvaluator();
- EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate);
+ RuleRegistry.AddRule("fractional", new FractionalEvaluator());
var targetingString = @"{""fractional"": [
{
@@ -106,19 +106,21 @@ public void EvaluateUsingDefaultWeights(string email, string flagKey, string exp
{ ""var"":""email"" }
]
},
- [""red""], [""blue""], [""green""], [""yellow""],
+ [""red""], [""blue""], [""green""], [""yellow""]
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary {
- { "email", email },
- {"$flagd", new Dictionary { {"flagKey", flagKey } } }
- };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "email", email },
+ { "$flagd", new Dictionary { { "flagKey", flagKey } } }
+ }));
+
+ // Act
+ var result = JsonLogic.Apply(rule, data);
- // Act & Assert
- var result = evaluator.Apply(rule, data);
+ // Assert
Assert.Equal(expected, result.ToString());
}
@@ -127,26 +129,25 @@ public void EvaluateUsingDefaultWeights(string email, string flagKey, string exp
public void EvaluateUsingTargetingKey(string flagKey, string expected)
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var fractionalEvaluator = new FractionalEvaluator();
- EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate);
+ RuleRegistry.AddRule("fractional", new FractionalEvaluator());
var targetingString = @"{""fractional"": [
- [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25],
+ [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25]
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary {
- { "targetingKey", "myKey" },
- {"$flagd", new Dictionary { {"flagKey", flagKey } } }
- };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "targetingKey", "myKey" },
+ { "$flagd", new Dictionary { { "flagKey", flagKey } } }
+ }));
- // Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.Equal(expected, result.ToString());
+ // Act
+ var result = JsonLogic.Apply(rule, data);
+ // Assert
+ Assert.Equal(expected, result.ToString());
}
}
}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs
index 930250a2..7a84a913 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Immutable;
using AutoFixture;
using OpenFeature.Constant;
@@ -10,7 +11,6 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test
{
public class UnitTestJsonEvaluator
{
-
[Fact]
public void TestJsonEvaluatorAddFlagConfig()
{
@@ -23,7 +23,6 @@ public void TestJsonEvaluatorAddFlagConfig()
var result = jsonEvaluator.ResolveBooleanValueAsync("validFlag", false);
Assert.True(result.Value);
-
}
[Fact]
@@ -40,7 +39,6 @@ public void TestJsonEvaluatorAddStaticStringEvaluation()
Assert.Equal("#CC0000", result.Value);
Assert.Equal("red", result.Variant);
Assert.Equal(Reason.Static, result.Reason);
-
}
[Fact]
@@ -57,7 +55,7 @@ public void TestJsonEvaluatorDynamicBoolEvaluation()
var builder = EvaluationContext.Builder();
builder
- .Set("color", "yellow");
+ .Set("color", "yellow");
var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlag", false, builder.Build());
@@ -80,7 +78,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyFlagKey()
var builder = EvaluationContext.Builder();
- var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build());
+ var result =
+ jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build());
Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
@@ -101,7 +100,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyTimestamp()
var builder = EvaluationContext.Builder();
- var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, builder.Build());
+ var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false,
+ builder.Build());
Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
@@ -119,7 +119,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluator()
var builder = EvaluationContext.Builder().Set("email", "test@faas.com");
- var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build());
+ var result =
+ jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build());
Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
@@ -137,7 +138,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluatorReturningBoolTy
var builder = EvaluationContext.Builder().Set("email", "test@faas.com");
- var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, builder.Build());
+ var result =
+ jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false,
+ builder.Build());
Assert.True(result.Value);
Assert.Equal("true", result.Variant);
@@ -155,7 +158,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithMissingDefaultVariant()
var builder = EvaluationContext.Builder();
- Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, builder.Build()));
+ Assert.Throws(() =>
+ jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false,
+ builder.Build()));
}
[Fact]
@@ -169,7 +174,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithUnexpectedVariantType()
var builder = EvaluationContext.Builder();
- Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, builder.Build()));
+ Assert.Throws(() =>
+ jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false,
+ builder.Build()));
}
[Fact]
@@ -186,7 +193,7 @@ public void TestJsonEvaluatorDynamicStringEvaluation()
var builder = EvaluationContext.Builder();
builder
- .Set("color", "yellow");
+ .Set("color", "yellow");
var result = jsonEvaluator.ResolveStringValueAsync("targetingStringFlag", "", builder.Build());
@@ -209,7 +216,7 @@ public void TestJsonEvaluatorDynamicFloatEvaluation()
var builder = EvaluationContext.Builder();
builder
- .Set("color", "yellow");
+ .Set("color", "yellow");
var result = jsonEvaluator.ResolveDoubleValueAsync("targetingFloatFlag", 0, builder.Build());
@@ -232,7 +239,7 @@ public void TestJsonEvaluatorDynamicIntEvaluation()
var builder = EvaluationContext.Builder();
builder
- .Set("color", "yellow");
+ .Set("color", "yellow");
var result = jsonEvaluator.ResolveIntegerValueAsync("targetingNumberFlag", 0, builder.Build());
@@ -255,7 +262,7 @@ public void TestJsonEvaluatorDynamicObjectEvaluation()
var builder = EvaluationContext.Builder();
builder
- .Set("color", "yellow");
+ .Set("color", "yellow");
var result = jsonEvaluator.ResolveStructureValueAsync("targetingObjectFlag", null, builder.Build());
@@ -280,7 +287,8 @@ public void TestJsonEvaluatorDisabledBoolEvaluation()
builder
.Set("color", "yellow");
- Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build()));
+ Assert.Throws(() =>
+ jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build()));
}
[Fact]
@@ -299,7 +307,8 @@ public void TestJsonEvaluatorFlagNotFoundEvaluation()
builder
.Set("color", "yellow");
- Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build()));
+ Assert.Throws(() =>
+ jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build()));
}
[Fact]
@@ -318,7 +327,80 @@ public void TestJsonEvaluatorWrongTypeEvaluation()
builder
.Set("color", "yellow");
- Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build()));
+ Assert.Throws(() =>
+ jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build()));
+ }
+
+ [Fact]
+ public void TestJsonEvaluatorReturnsFlagMetadata()
+ {
+ var fixture = new Fixture();
+
+ var jsonEvaluator = new JsonEvaluator(fixture.Create());
+
+ jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.flags);
+
+ var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false);
+ Assert.NotNull(result.FlagMetadata);
+ Assert.Equal("1.0.2", result.FlagMetadata.GetString("string"));
+ Assert.Equal(2, result.FlagMetadata.GetDouble("integer"));
+ Assert.Equal(true, result.FlagMetadata.GetBool("boolean"));
+ Assert.Equal(.1, result.FlagMetadata.GetDouble("float"));
+ }
+
+ [Fact]
+ public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetadata()
+ {
+ var fixture = new Fixture();
+
+ var jsonEvaluator = new JsonEvaluator(fixture.Create());
+
+ jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags);
+
+ var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false);
+ Assert.NotNull(result.FlagMetadata);
+ Assert.Equal("1.0.3", result.FlagMetadata.GetString("string"));
+ Assert.Equal(3, result.FlagMetadata.GetDouble("integer"));
+ Assert.Equal(false, result.FlagMetadata.GetBool("boolean"));
+ Assert.Equal(.2, result.FlagMetadata.GetDouble("float"));
+ }
+
+ [Fact]
+ public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata()
+ {
+ var fixture = new Fixture();
+
+ var jsonEvaluator = new JsonEvaluator(fixture.Create());
+
+ jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags);
+
+ var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false);
+
+ Assert.NotNull(result.FlagMetadata);
+ Assert.Equal("1.0.2", result.FlagMetadata.GetString("string"));
+ Assert.Equal(2, result.FlagMetadata.GetDouble("integer"));
+ Assert.Equal(true, result.FlagMetadata.GetBool("boolean"));
+ Assert.Equal(.1, result.FlagMetadata.GetDouble("float"));
+ }
+
+ [Fact]
+ public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata()
+ {
+ var fixture = new Fixture();
+
+ var jsonEvaluator = new JsonEvaluator(fixture.Create());
+
+ Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata));
+ }
+
+ [Fact]
+ public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata()
+ {
+ var fixture = new Fixture();
+
+ var jsonEvaluator = new JsonEvaluator(fixture.Create());
+
+ Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata));
}
}
}
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs
index 46ad97a5..cad81516 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
-using JsonLogic.Net;
-using Newtonsoft.Json.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
using Xunit;
@@ -8,344 +9,323 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test
{
public class SemVerEvaluatorTest
{
-
[Fact]
public void EvaluateVersionEqual()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""="",
""1.0.0""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.0" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.0" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.1");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.1" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionNotEqual()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""!="",
""1.0.0""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.0" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.0" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.1");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.1" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
}
[Fact]
public void EvaluateVersionLess()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""<"",
""1.0.2""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.1" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.1" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.2");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.2" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionLessOrEqual()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""<="",
""1.0.2""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.1" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.1" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.2");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.2" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.3");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.3" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionGreater()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
"">"",
""1.0.2""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.3" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.3" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.2");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.2" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionGreaterOrEqual()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
"">="",
""1.0.2""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.2" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.2" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.3");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.3" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "1.0.1");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.1" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionMatchMajor()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""^"",
""1.0.0""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.0.3" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.0.3" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "2.0.0");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "2.0.0" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionMatchMinor()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""~"",
""1.3.0""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.3.3" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.3.3" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("version", "2.3.0");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "2.3.0" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionTooFewArguments()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""~""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.3.3" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.3.3" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EvaluateVersionNotAValidVersion()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var semVerEvaluator = new SemVerEvaluator();
- EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate);
+ RuleRegistry.AddRule("sem_ver", new SemVerRule());
var targetingString = @"{""sem_ver"": [
- {
- ""var"": [
- ""version""
- ]
- },
+ { ""var"": ""version"" },
""~"",
""test""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "version", "1.3.3" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "version", "1.3.3" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
}
}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs
index 6d59977d..ae3238bc 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
-using JsonLogic.Net;
-using Newtonsoft.Json.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Logic;
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
using Xunit;
@@ -8,177 +9,156 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test
{
public class StringEvaluatorTest
{
-
[Fact]
public void StartsWith()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.StartsWith);
+ RuleRegistry.AddRule("starts_with", new StartsWithRule());
var targetingString = @"{""starts_with"": [
- {
- ""var"": [
- ""color""
- ]
- },
+ { ""var"": ""color"" },
""yellow""
]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", "yellowcolor" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", "yellowcolor" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("color", "blue");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", "blue" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EndsWith()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith);
+ RuleRegistry.AddRule("ends_with", new EndsWithRule());
var targetingString = @"{""ends_with"": [
- {
- ""var"": [
- ""color""
- ]
- },
- ""purple""
- ]}";
+ { ""var"": ""color"" },
+ ""purple""
+ ]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", "deep-purple" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", "deep-purple" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.True(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.True(result.GetValue());
- data.Clear();
- data.Add("color", "purple-nightmare");
+ data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", "purple-nightmare" }
+ }));
- result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void NonStringTypeInRule()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith);
+ RuleRegistry.AddRule("ends_with", new EndsWithRule());
var targetingString = @"{""ends_with"": [
- {
- ""var"": [
- ""color""
- ]
- },
- 1
- ]}";
+ { ""var"": ""color"" },
+ 1
+ ]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", "deep-purple" } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", "deep-purple" }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void NonStringTypeInData()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith);
+ RuleRegistry.AddRule("ends_with", new EndsWithRule());
var targetingString = @"{""ends_with"": [
- {
- ""var"": [
- ""color""
- ]
- },
- ""green""
- ]}";
+ { ""var"": ""color"" },
+ ""green""
+ ]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", 5 } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", 5 }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void EndsWithNotEnoughArguments()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith);
+ RuleRegistry.AddRule("ends_with", new EndsWithRule());
var targetingString = @"{""ends_with"": [
- {
- ""var"": [
- ""color""
- ]
- }
- ]}";
+ { ""var"": ""color"" }
+ ]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", 5 } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", 5 }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
[Fact]
public void StartsWithNotEnoughArguments()
{
// Arrange
- var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default);
- var stringEvaluator = new StringEvaluator();
- EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.EndsWith);
+ RuleRegistry.AddRule("starts_with", new StartsWithRule());
var targetingString = @"{""starts_with"": [
- {
- ""var"": [
- ""color""
- ]
- }
- ]}";
+ { ""var"": ""color"" }
+ ]}";
- // Parse json into hierarchical structure
- var rule = JObject.Parse(targetingString);
+ var rule = JsonNode.Parse(targetingString);
- var data = new Dictionary { { "color", 5 } };
+ var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary
+ {
+ { "color", 5 }
+ }));
// Act & Assert
- var result = evaluator.Apply(rule, data);
- Assert.False(result.IsTruthy());
+ var result = JsonLogic.Apply(rule, data);
+ Assert.False(result.GetValue());
}
}
}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs
index c1db7aaa..18aa3dff 100644
--- a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs
+++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs
@@ -162,7 +162,7 @@ public class Utils
},
""defaultVariant"": ""bool2"",
""targeting"": {
- ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""]
+ ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""]
}
},
""targetingBoolFlagUsingSharedEvaluatorReturningBoolType"": {
@@ -173,7 +173,7 @@ public class Utils
},
""defaultVariant"": ""true"",
""targeting"": {
- ""if"": [{ $ref: ""emailWithFaas"" }, true]
+ ""if"": [{ ""$ref"": ""emailWithFaas"" }, true]
}
},
""targetingBoolFlagWithMissingDefaultVariant"": {
@@ -184,7 +184,7 @@ public class Utils
},
""defaultVariant"": ""true"",
""targeting"": {
- ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""]
+ ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""]
}
},
""targetingBoolFlagWithUnexpectedVariantType"": {
@@ -195,7 +195,7 @@ public class Utils
},
""defaultVariant"": ""true"",
""targeting"": {
- ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""]
+ ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""]
}
},
""targetingStringFlag"": {
@@ -301,10 +301,95 @@ public class Utils
""off"": false
},
""defaultVariant"": ""on""
+ },
+ ""metadata-flag"": {
+ ""state"": ""ENABLED"",
+ ""variants"": {
+ ""on"": true,
+ ""off"": false
+ },
+ ""defaultVariant"": ""on"",
+ ""metadata"": {
+ ""string"": ""1.0.2"",
+ ""integer"": 2,
+ ""boolean"": true,
+ ""float"": 0.1
+ }
+ }
+ }
+}";
+
+ public static string metadataFlags = @"{
+ ""flags"":{
+ ""metadata-flag"": {
+ ""state"": ""ENABLED"",
+ ""variants"": {
+ ""on"": true,
+ ""off"": false
+ },
+ ""defaultVariant"": ""on"",
+ ""metadata"":{
+ ""string"": ""1.0.2"",
+ ""integer"": 2,
+ ""boolean"": true,
+ ""float"": 0.1
+ }
+ },
+ ""without-metadata-flag"": {
+ ""state"": ""ENABLED"",
+ ""variants"": {
+ ""on"": true,
+ ""off"": false
+ },
+ ""defaultVariant"": ""on""
}
+ },
+ ""metadata"": {
+ ""string"": ""1.0.3"",
+ ""integer"": 3,
+ ""boolean"": false,
+ ""float"": 0.2
}
}";
+ public static string invalidFlagSetMetadata = @"{
+ ""flags"":{
+ ""without-metadata-flag"": {
+ ""state"": ""ENABLED"",
+ ""variants"": {
+ ""on"": true,
+ ""off"": false
+ },
+ ""defaultVariant"": ""on""
+ }
+ },
+ ""metadata"": {
+ ""string"": {""in"": ""valid""},
+ ""integer"": 3,
+ ""boolean"": false,
+ ""float"": 0.2
+ }
+}";
+ public static string invalidFlagMetadata = @"{
+ ""flags"":{
+ ""invalid-metadata-flag"": {
+ ""state"": ""ENABLED"",
+ ""variants"": {
+ ""on"": true,
+ ""off"": false
+ },
+ ""defaultVariant"": ""on"",
+ ""metadata"": {
+ ""string"": ""1.0.2"",
+ ""integer"": 2,
+ ""boolean"": true,
+ ""float"": {""in"": ""valid""}
+ }
+ }
+ }
+}";
+
+
///
/// Repeatedly runs the supplied assertion until it doesn't throw, or the timeout is reached.
///
@@ -312,11 +397,11 @@ public class Utils
/// Timeout in millis (defaults to 1000)
/// Poll interval (defaults to 100
///
- public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, int pollIntervalMillis = 100)
+ public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000,
+ int pollIntervalMillis = 100)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken)))
{
-
cts.CancelAfter(timeoutMillis);
var exceptions = new List();
@@ -347,6 +432,7 @@ public static async Task AssertUntilAsync(Action assertionFun
throw new AggregateException(message, exceptions);
}
}
+
throw new AggregateException(message, exceptions);
}
}
diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs
new file mode 100644
index 00000000..bb3e0ea9
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs
@@ -0,0 +1,156 @@
+using OpenFeature.Contrib.Providers.Flipt.Converters;
+using OpenFeature.Model;
+using System.Text.Json;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Test;
+
+public class FliptExtensionsTest
+{
+ [Fact]
+ public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary()
+ {
+ var evaluationContext = EvaluationContext.Builder().Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContext_ShouldReturnADictionaryWithValues()
+ {
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("location", "somewhere")
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("location", result.Keys);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndIntegerValue_ShouldReturnADictionaryWithStringValues()
+ {
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("age", 23)
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("age", result.Keys);
+
+ var actual = result["age"];
+ Assert.Equal("23", actual);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndValuesOfStrings_ShouldReturnADictionaryWithSerializedStringValues()
+ {
+ var testStructure = new Structure(new Dictionary
+ {
+ { "config1", new Value("value1") },
+ { "config2", new Value("value2") }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("config", testStructure)
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("config", result.Keys);
+
+ var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings);
+ var actual = result["config"];
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndMixedValueTypes_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var testStructure = new Structure(new Dictionary
+ {
+ { "config1", new Value(1) },
+ { "config2", new Value("value2") },
+ { "config3", new Value(DateTime.Now) }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("config", testStructure)
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("config", result.Keys);
+
+ var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings);
+ var actual = result["config"];
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var sampleDictionary = new Dictionary();
+ sampleDictionary["config2"] = new Value([
+ new Value([new Value("element1-1"), new Value("element1-2")]),
+ new Value("element2"),
+ new Value("element3")
+ ]);
+ sampleDictionary["config3"] = new Value(DateTime.Now);
+
+ var testStructure = new Structure(sampleDictionary);
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("config", testStructure)
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("config", result.Keys);
+
+ var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings);
+ var actual = result["config"];
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextWithNestedStructure_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var testStructure = new Structure(new Dictionary
+ {
+ {
+ "config-value-struct", new Value(new Structure(new Dictionary
+ {
+ { "nested1", new Value(1) }
+ }))
+ },
+ { "config-value-value", new Value(new Value(DateTime.Now)) }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey(Guid.NewGuid().ToString())
+ .Set("config", testStructure)
+ .Build();
+ var result = evaluationContext.ToStringDictionary();
+
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+ Assert.Contains("config", result.Keys);
+
+ var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings);
+ var actual = result["config"];
+ Assert.Equal(expected, actual);
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs
new file mode 100644
index 00000000..412a6294
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs
@@ -0,0 +1,133 @@
+using Flipt.Rest;
+using Moq;
+using OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
+using OpenFeature.Error;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Test;
+
+public class FliptProviderTest
+{
+ private readonly string _fliptUrl = "http://localhost:8080/";
+
+ [Fact]
+ public void CreateFliptProvider_ShouldReturnFliptProvider()
+ {
+ // Flipt library always returns a flipt instance
+ var fliptProvider = new FliptProvider(_fliptUrl);
+ Assert.NotNull(fliptProvider);
+ }
+
+ [Fact]
+ public void CreateFliptProvider_GivenEmptyUrl_ShouldThrowInvalidOperationException()
+ {
+ Assert.Throws(() => new FliptProvider(""));
+ }
+
+ [Fact]
+ public async Task
+ ResolveNonBooleansAsync_GivenFlagThatHasATypeMismatch_ShouldReturnDefaultValueWithTypeMismatchError()
+ {
+ var mockFliptClientWrapper = new Mock();
+ const string flagKey = "iamnotadouble";
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ReturnsAsync(new VariantEvaluationResponse
+ {
+ FlagKey = flagKey,
+ VariantKey = "variant-key",
+ RequestId = Guid.NewGuid()
+ .ToString(),
+ SegmentKeys = ["segment1"],
+ VariantAttachment = "",
+ Match = true,
+ Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON
+ });
+
+ var provider = new FliptProvider(new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object));
+
+ await Assert.ThrowsAsync(async () => await provider.ResolveDoubleValueAsync(flagKey, 0.0));
+ }
+
+ [Fact]
+ public async Task ResolveStringValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper()
+ {
+ const string flagKey = "feature-flag-key";
+ var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey);
+ await provider.ResolveStringValueAsync(flagKey, "");
+ mockFliptClientWrapper.Verify(
+ fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once);
+ }
+
+ [Fact]
+ public async Task ResolveDoubleValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper()
+ {
+ const string flagKey = "feature-flag-key";
+ var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "0.0");
+ await provider.ResolveDoubleValueAsync(flagKey, 0.0);
+ mockFliptClientWrapper.Verify(
+ fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once);
+ }
+
+ [Fact]
+ public async Task ResolveIntegerValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper()
+ {
+ const string flagKey = "feature-flag-key";
+ var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "0");
+ await provider.ResolveIntegerValueAsync(flagKey, 0);
+ mockFliptClientWrapper.Verify(
+ fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once);
+ }
+
+ [Fact]
+ public async Task ResolveStructureValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper()
+ {
+ const string flagKey = "feature-flag-key";
+ var (provider, mockFliptClientWrapper) =
+ GenerateFliptProviderWithMockedDependencies(flagKey, new Value().AsString!);
+ await provider.ResolveStructureValueAsync(flagKey, new Value());
+ mockFliptClientWrapper.Verify(
+ fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once);
+ }
+
+ [Fact]
+ public async Task ResolveBooleanValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper()
+ {
+ const string flagKey = "feature-flag-key";
+ var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "true");
+ await provider.ResolveBooleanValueAsync(flagKey, false);
+ mockFliptClientWrapper.Verify(
+ fcw => fcw.EvaluateBooleanAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once);
+ }
+
+ private static (FliptProvider, Mock) GenerateFliptProviderWithMockedDependencies(
+ string flagKey, string variantKey = "variant-key")
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ReturnsAsync(new VariantEvaluationResponse
+ {
+ FlagKey = flagKey,
+ VariantKey = variantKey,
+ RequestId = Guid.NewGuid()
+ .ToString(),
+ SegmentKeys = ["segment1"],
+ VariantAttachment = "",
+ Match = true,
+ Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON
+ });
+
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny()))
+ .ReturnsAsync(new BooleanEvaluationResponse
+ {
+ FlagKey = flagKey,
+ RequestId = Guid.NewGuid()
+ .ToString(),
+ Enabled = true,
+ Reason = BooleanEvaluationResponseReason.MATCH_EVALUATION_REASON
+ });
+
+ return (new FliptProvider(new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object)),
+ mockFliptClientWrapper);
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs
new file mode 100644
index 00000000..4d6026d9
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs
@@ -0,0 +1,196 @@
+// ReSharper disable RedundantUsingDirective
+
+using Flipt.Rest;
+using Moq;
+using OpenFeature.Constant;
+using OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
+using OpenFeature.Contrib.Providers.Flipt.Converters;
+using OpenFeature.Model;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Test;
+
+public class FliptToOpenFeatureConverterTest
+{
+ // EvaluateBooleanAsync Tests
+ [Theory]
+ [InlineData(HttpStatusCode.NotFound, false)]
+ [InlineData(HttpStatusCode.BadRequest, false)]
+ [InlineData(HttpStatusCode.InternalServerError, false)]
+ [InlineData(HttpStatusCode.Forbidden, false)]
+ [InlineData(HttpStatusCode.Ambiguous, false)]
+ public async Task EvaluateBooleanAsync_GivenHttpRequestException_ShouldHandleHttpRequestException(
+ HttpStatusCode thrownStatusCode, bool fallbackValue)
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw =>
+ fcw.EvaluateBooleanAsync(It.IsAny()))
+ .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null));
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+
+ await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallbackValue));
+ }
+
+ [Theory]
+ [InlineData("show-feature", true)]
+ [InlineData("show-feature", false)]
+ public async Task EvaluateBooleanAsync_GivenExistingFlag_ShouldReturnFlagValue(string flagKey,
+ bool valueFromSrc)
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny()))
+ .ReturnsAsync(new BooleanEvaluationResponse
+ {
+ Enabled = valueFromSrc,
+ FlagKey = flagKey,
+ RequestId = Guid.NewGuid().ToString()
+ });
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+ var resolution = await fliptToOpenFeature.EvaluateBooleanAsync("show-feature", false);
+
+ Assert.Equal(flagKey, resolution.FlagKey);
+ Assert.Equal(valueFromSrc, resolution.Value);
+ Assert.Equal(Reason.TargetingMatch, resolution.Reason);
+ }
+
+ [Theory]
+ [InlineData("show-feature", false)]
+ [InlineData("show-feature", true)]
+ public async Task EvaluateBooleanAsync_GivenNonExistentFlag_ShouldReturnDefaultValueWithFlagNotFoundError(
+ string flagKey, bool fallBackValue)
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny()))
+ .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null));
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+
+ await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateBooleanAsync(flagKey, fallBackValue));
+ }
+
+ // EvaluateAsync Tests
+
+ [Theory]
+ [InlineData(HttpStatusCode.NotFound, 0.0)]
+ [InlineData(HttpStatusCode.BadRequest, 0.0)]
+ [InlineData(HttpStatusCode.InternalServerError, 0.0)]
+ [InlineData(HttpStatusCode.Forbidden, 0.0)]
+ [InlineData(HttpStatusCode.Ambiguous, 0.0)]
+ public async Task EvaluateAsync_GivenHttpRequestException_ShouldHandleHttpRequestException(
+ HttpStatusCode thrownStatusCode, double fallbackValue)
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw =>
+ fcw.EvaluateVariantAsync(It.IsAny()))
+ .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null));
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+
+ await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("flagKey", fallbackValue));
+ }
+
+ [Theory]
+ [InlineData("variant-flag", 1.0, 1.0)]
+ [InlineData("variant-flag", "thisisastring", "thisisastring")]
+ [InlineData("variant-flag", 1, 1)]
+ public async Task EvaluateAsync_GivenExistingVariantFlagWhichIsNotAnObject_ShouldReturnFlagValue(string flagKey,
+ object valueFromSrc, object? expectedValue = null, string variantAttachment = "")
+ {
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ReturnsAsync(new VariantEvaluationResponse
+ {
+ FlagKey = flagKey,
+ VariantKey = valueFromSrc.ToString() ?? string.Empty,
+ RequestId = Guid.NewGuid().ToString(),
+ SegmentKeys = ["segment1"],
+ VariantAttachment = variantAttachment,
+ Match = true,
+ Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON
+ });
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+ var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, valueFromSrc);
+
+ Assert.Equal(flagKey, resolution.FlagKey);
+ Assert.Equal(valueFromSrc.ToString() ?? string.Empty, resolution.Value);
+ Assert.Equal(expectedValue?.ToString(), resolution.Value);
+ Assert.Equal(Reason.TargetingMatch, resolution.Reason);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_GivenExistingVariantFlagAndWithAnObject_ShouldReturnFlagValue()
+ {
+ const string flagKey = "variant-flag";
+ const string variantKey = "variant-A";
+ const string valueFromSrc = """
+ {
+ "name": "Mr. Robinson",
+ "age": 12,
+ }
+ """;
+ var expectedValue = new Value(new Structure(new Dictionary
+ {
+ { "name", new Value("Mr. Robinson") }, { "age", new Value(12) }
+ }));
+
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ReturnsAsync(new VariantEvaluationResponse
+ {
+ FlagKey = flagKey,
+ VariantKey = variantKey,
+ RequestId = Guid.NewGuid().ToString(),
+ SegmentKeys = ["segment1"],
+ VariantAttachment = valueFromSrc,
+ Match = true,
+ Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON
+ });
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+ var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, new Value());
+
+ Assert.Equal(flagKey, resolution.FlagKey);
+ Assert.Equal(variantKey, resolution.Variant);
+
+ var expected = JsonSerializer.Serialize(expectedValue, JsonConverterExtensions.DefaultSerializerSettings);
+ var actual = JsonSerializer.Serialize(resolution.Value, JsonConverterExtensions.DefaultSerializerSettings);
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task
+ EvaluateVariantAsync_GivenNonExistentFlagWithNonNestedFallback_ShouldReturnDefaultValueWithFlagNotFoundError()
+ {
+ var fallbackValue = new Value(new Structure(new Dictionary
+ {
+ { "name", new Value("Mr. Robinson") }, { "age", new Value(12) }
+ }));
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null));
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+
+ await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue));
+ }
+
+ [Fact]
+ public async Task
+ EvaluateVariantAsync_GivenNonExistentFlagWithNestedFallback_ShouldReturnDefaultValueWithFlagNotFoundError()
+ {
+ var fallbackValue = new Value("");
+ var mockFliptClientWrapper = new Mock();
+ mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny()))
+ .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null));
+
+ var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object);
+
+ await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue));
+ }
+}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj
new file mode 100644
index 00000000..2ab09f7a
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj
@@ -0,0 +1,22 @@
+
+
+
+ enable
+ enable
+
+ false
+ true
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs
index 7f433219..6ab3ce47 100644
--- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs
+++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs
@@ -1,563 +1,665 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Text.Json;
-using System.Threading.Tasks;
-using OpenFeature.Constant;
-using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
-using OpenFeature.Model;
-using RichardSzalay.MockHttp;
-using Xunit;
-
-namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
-
-public class GoFeatureFlagProviderTest
-{
- private static readonly string baseUrl = "http://gofeatureflag.org";
- private static readonly string prefixEval = baseUrl + "/v1/feature/";
- private static readonly string suffixEval = "/eval";
- private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx();
- private readonly HttpMessageHandler _mockHttp = InitMock();
-
- private static HttpMessageHandler InitMock()
- {
- const string mediaType = "application/json";
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When($"{prefixEval}fail_500{suffixEval}").Respond(HttpStatusCode.InternalServerError);
- mockHttp.When($"{prefixEval}api_key_missing{suffixEval}").Respond(HttpStatusCode.BadRequest);
- mockHttp.When($"{prefixEval}invalid_api_key{suffixEval}").Respond(HttpStatusCode.Unauthorized);
- mockHttp.When($"{prefixEval}flag_not_found{suffixEval}").Respond(HttpStatusCode.NotFound);
- mockHttp.When($"{prefixEval}bool_targeting_match{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":true}");
- mockHttp.When($"{prefixEval}disabled{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":true}");
- mockHttp.When($"{prefixEval}disabled_double{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":100.25}");
- mockHttp.When($"{prefixEval}disabled_integer{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":100}");
- mockHttp.When($"{prefixEval}disabled_object{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":null}");
- mockHttp.When($"{prefixEval}disabled_string{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":\"\"}");
- mockHttp.When($"{prefixEval}double_key{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":100.25}");
- mockHttp.When($"{prefixEval}flag_not_found{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"SdkDefault\",\"failed\":true,\"version\":\"\",\"reason\":\"ERROR\",\"errorCode\":\"FLAG_NOT_FOUND\",\"value\":\"false\"}");
- mockHttp.When($"{prefixEval}integer_key{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":100}");
- mockHttp.When($"{prefixEval}list_key{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":[\"test\",\"test1\",\"test2\",\"false\",\"test3\"]}");
- mockHttp.When($"{prefixEval}object_key{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":{\"test\":\"test1\",\"test2\":false,\"test3\":123.3,\"test4\":1,\"test5\":null}}");
- mockHttp.When($"{prefixEval}string_key{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":\"CC0000\"}");
- mockHttp.When($"{prefixEval}unknown_reason{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"CUSTOM_REASON\",\"errorCode\":\"\",\"value\":true}");
- mockHttp.When($"{prefixEval}does_not_exists{suffixEval}").Respond(mediaType,
- "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":true,\"version\":\"\",\"reason\":\"ERROR\",\"errorCode\":\"FLAG_NOT_FOUND\",\"value\":\"\"}");
- return mockHttp;
- }
-
- private static EvaluationContext InitDefaultEvaluationCtx()
- {
- return EvaluationContext.Builder()
- .Set("targetingKey", "d45e303a-38c2-11ed-a261-0242ac120002")
- .Set("email", "john.doe@gofeatureflag.org")
- .Set("firstname", "john")
- .Set("lastname", "doe")
- .Set("anonymous", false)
- .Set("professional", true)
- .Set("rate", 3.14)
- .Set("age", 30)
- .Set("company_info", new Value(new Structure(new Dictionary
- {
- { "name", new Value("my_company") },
- { "size", new Value(120) }
- })))
- .Set("labels", new Value(new List
- {
- new("pro"),
- new("beta")
- }))
- .Build();
- }
-
-
- [Fact]
- public async Task getMetadata_validate_name()
- {
- var goFeatureFlagProvider = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Timeout = new TimeSpan(19 * TimeSpan.TicksPerHour),
- Endpoint = baseUrl
- });
- await Api.Instance.SetProviderAsync(goFeatureFlagProvider);
- Assert.Equal("GO Feature Flag Provider", Api.Instance.GetProvider().GetMetadata().Name);
- }
-
-
- [Fact]
- private void constructor_options_null()
- {
- Assert.Throws(() => new GoFeatureFlagProvider(null));
- }
-
- [Fact]
- private void constructor_options_empty()
- {
- Assert.Throws(() => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions()));
- }
-
- [Fact]
- private void constructor_options_empty_endpoint()
- {
- Assert.Throws(
- () => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = "" }));
- }
-
- [Fact]
- private void constructor_options_only_timeout()
- {
- Assert.Throws(
- () => new GoFeatureFlagProvider(
- new GoFeatureFlagProviderOptions { Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) }
- )
- );
- }
-
- [Fact]
- private void constructor_options_valid_endpoint()
- {
- var exception = Record.Exception(() =>
- new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = baseUrl }));
- Assert.Null(exception);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_endpoint_not_available()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("fail_500", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(ErrorType.General, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_have_bad_request_if_no_token()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("api_key_missing", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(Reason.Error, res.Result.Reason);
- Assert.Equal(ErrorType.General, res.Result.ErrorType);
- }
-
- [Fact]
- public async Task should_have_unauthorized_if_invalid_token()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond),
- ApiKey = "ff877c7a-4594-43b5-89a8-df44c9984bd8"
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("invalid_api_key", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(Reason.Error, res.Result.Reason);
- Assert.Equal(ErrorType.General, res.Result.ErrorType);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_flag_does_not_exists()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("flag_not_found", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_we_expect_a_boolean_and_got_another_type()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("string_key", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_boolean_flag_with_TARGETING_MATCH_reason()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("bool_targeting_match", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.True(res.Result.Value);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_return_custom_reason_if_returned_by_relay_proxy()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("unknown_reason", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.True(res.Result.Value);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal("CUSTOM_REASON", res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_boolean_default_value_if_the_flag_is_disabled()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetBooleanDetailsAsync("disabled", false, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.False(res.Result.Value);
- Assert.Equal(Reason.Disabled, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_type()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetStringDetailsAsync("bool_targeting_match", "default", _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal("default", res.Result.Value);
- Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetStringDetailsAsync("string_key", "defaultValue", _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal("CC0000", res.Result.Value);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_string_default_value_if_the_flag_is_disabled()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetStringDetailsAsync("disabled_string", "defaultValue", _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal("defaultValue", res.Result.Value);
- Assert.Equal(Reason.Disabled, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_we_expect_a_integer_and_got_another_type()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetIntegerDetailsAsync("string_key", 200, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(200, res.Result.Value);
- Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_integer_flag_with_TARGETING_MATCH_reason()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetIntegerDetailsAsync("integer_key", 1200, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(100, res.Result.Value);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_integer_default_value_if_the_flag_is_disabled()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(1225, res.Result.Value);
- Assert.Equal(Reason.Disabled, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_throw_an_error_if_we_expect_a_integer_and_double_type()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetIntegerDetailsAsync("double_key", 200, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(200, res.Result.Value);
- Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_double_flag_with_TARGETING_MATCH_reason()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetDoubleDetailsAsync("double_key", 1200.25, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(100.25, res.Result.Value);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_double_default_value_if_the_flag_is_disabled()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetDoubleDetailsAsync("disabled_double", 1225.34, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(1225.34, res.Result.Value);
- Assert.Equal(Reason.Disabled, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_value_flag_with_TARGETING_MATCH_reason()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetObjectDetailsAsync("object_key", null, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- var want = JsonSerializer.Serialize(new Value(new Structure(new Dictionary
- {
- { "test", new Value("test1") }, { "test2", new Value(false) }, { "test3", new Value(123.3) },
- { "test4", new Value(1) }
- })));
- Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value));
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_wrap_into_value_if_wrong_type()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetObjectDetailsAsync("string_key", null, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(new Value("CC0000").AsString, res.Result.Value.AsString);
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_object_default_value_if_the_flag_is_disabled()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetObjectDetailsAsync("disabled_object", new Value("default"), _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(new Value("default").AsString, res.Result.Value.AsString);
- Assert.Equal(Reason.Disabled, res.Result.Reason);
- }
-
-
- [Fact]
- public async Task should_throw_an_error_if_no_targeting_key()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetStringDetailsAsync("list_key", "empty", EvaluationContext.Empty);
- Assert.NotNull(res.Result);
- Assert.Equal("empty", res.Result.Value);
- Assert.Equal(ErrorType.InvalidContext, res.Result.ErrorType);
- Assert.Equal(Reason.Error, res.Result.Reason);
- }
-
- [Fact]
- public async Task should_resolve_a_valid_value_flag_with_a_list()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetObjectDetailsAsync("list_key", null, _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- var want = JsonSerializer.Serialize(new Value(new List
- { new("test"), new("test1"), new("test2"), new("false"), new("test3") }));
- Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value));
- Assert.Equal(ErrorType.None, res.Result.ErrorType);
- Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
- Assert.Equal("True", res.Result.Variant);
- }
-
- [Fact]
- public async Task should_use_object_default_value_if_flag_not_found()
- {
- var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
- {
- Endpoint = baseUrl,
- HttpMessageHandler = _mockHttp,
- Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
- });
- await Api.Instance.SetProviderAsync(g);
- var client = Api.Instance.GetClient("test-client");
- var res = client.GetObjectDetailsAsync("does_not_exists", new Value("default"), _defaultEvaluationCtx);
- Assert.NotNull(res.Result);
- Assert.Equal(new Value("default").AsString, res.Result.Value.AsString);
- Assert.Equal(Reason.Error, res.Result.Reason);
- Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType);
- Assert.Equal("flag does_not_exists was not found in your configuration", res.Result.ErrorMessage);
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using OpenFeature.Constant;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
+using OpenFeature.Model;
+using RichardSzalay.MockHttp;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
+
+public class GoFeatureFlagProviderTest
+{
+ private static readonly string baseUrl = "http://gofeatureflag.org";
+ private static readonly string prefixEval = baseUrl + "/ofrep/v1/evaluate/flags/";
+ private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx();
+ private readonly HttpMessageHandler _mockHttp = InitMock();
+
+ private static HttpMessageHandler InitMock()
+ {
+ const string mediaType = "application/json";
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When($"{prefixEval}fail_500").Respond(HttpStatusCode.InternalServerError);
+ mockHttp.When($"{prefixEval}api_key_missing").Respond(HttpStatusCode.BadRequest);
+ mockHttp.When($"{prefixEval}invalid_api_key").Respond(HttpStatusCode.Unauthorized);
+ mockHttp.When($"{prefixEval}flag_not_found").Respond(HttpStatusCode.NotFound);
+ mockHttp.When($"{prefixEval}bool_targeting_match").Respond(mediaType,
+ "{ \"value\":true, \"key\":\"bool_targeting_match\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true }");
+ mockHttp.When($"{prefixEval}disabled").Respond(mediaType,
+ "{ \"value\":false, \"key\":\"disabled\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}disabled_double").Respond(mediaType,
+ "{ \"value\":100.25, \"key\":\"disabled_double\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}disabled_integer").Respond(mediaType,
+ "{ \"value\":100, \"key\":\"disabled_integer\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}disabled_object").Respond(mediaType,
+ "{ \"value\":null, \"key\":\"disabled_object\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}disabled_string").Respond(mediaType,
+ "{ \"value\":\"\", \"key\":\"disabled_string\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}double_key").Respond(mediaType,
+ "{ \"value\":100.25, \"key\":\"double_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}flag_not_found").Respond(mediaType,
+ "{ \"value\":false, \"key\":\"flag_not_found\", \"reason\":\"FLAG_NOT_FOUND\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}integer_key").Respond(mediaType,
+ "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}list_key").Respond(mediaType,
+ "{ \"value\":[\"test\",\"test1\",\"test2\",\"false\",\"test3\"], \"key\":\"list_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}object_key").Respond(mediaType,
+ "{ \"value\":{\"test\":\"test1\",\"test2\":false,\"test3\":123.3,\"test4\":1,\"test5\":null}, \"key\":\"object_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}string_key").Respond(mediaType,
+ "{ \"value\":\"CC0000\", \"key\":\"string_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}unknown_reason").Respond(mediaType,
+ "{ \"value\":\"true\", \"key\":\"unknown_reason\", \"reason\":\"CUSTOM_REASON\", \"variant\":\"True\", \"cacheable\":true}");
+ mockHttp.When($"{prefixEval}does_not_exists").Respond(mediaType,
+ "{ \"value\":\"\", \"key\":\"does_not_exists\", \"errorCode\":\"FLAG_NOT_FOUND\", \"variant\":\"defaultSdk\", \"cacheable\":true, \"errorDetails\":\"flag does_not_exists was not found in your configuration\"}");
+ mockHttp.When($"{prefixEval}integer_with_metadata").Respond(mediaType,
+ "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}");
+ return mockHttp;
+ }
+
+ private static EvaluationContext InitDefaultEvaluationCtx()
+ {
+ return EvaluationContext.Builder()
+ .Set("targetingKey", "d45e303a-38c2-11ed-a261-0242ac120002")
+ .Set("email", "john.doe@gofeatureflag.org")
+ .Set("firstname", "john")
+ .Set("lastname", "doe")
+ .Set("anonymous", false)
+ .Set("professional", true)
+ .Set("rate", 3.14)
+ .Set("age", 30)
+ .Set("company_info", new Value(new Structure(new Dictionary
+ {
+ { "name", new Value("my_company") },
+ { "size", new Value(120) }
+ })))
+ .Set("labels", new Value(new List
+ {
+ new("pro"),
+ new("beta")
+ }))
+ .Build();
+ }
+
+
+ [Fact]
+ public async Task getMetadata_validate_name()
+ {
+ var goFeatureFlagProvider = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Timeout = new TimeSpan(19 * TimeSpan.TicksPerHour),
+ Endpoint = baseUrl
+ });
+ await Api.Instance.SetProviderAsync(goFeatureFlagProvider);
+ Assert.Equal("GO Feature Flag Provider", Api.Instance.GetProvider().GetMetadata().Name);
+ }
+
+
+ [Fact]
+ private void constructor_options_null()
+ {
+ Assert.Throws(() => new GoFeatureFlagProvider(null));
+ }
+
+ [Fact]
+ private void constructor_options_empty()
+ {
+ Assert.Throws(() => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions()));
+ }
+
+ [Fact]
+ private void constructor_options_empty_endpoint()
+ {
+ Assert.Throws(
+ () => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = "" }));
+ }
+
+ [Fact]
+ private void constructor_options_only_timeout()
+ {
+ Assert.Throws(
+ () => new GoFeatureFlagProvider(
+ new GoFeatureFlagProviderOptions { Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) }
+ )
+ );
+ }
+
+ [Fact]
+ private void constructor_options_valid_endpoint()
+ {
+ var exception = Record.Exception(() =>
+ new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = baseUrl }));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_endpoint_not_available()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("fail_500", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(ErrorType.General, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_have_bad_request_if_no_token()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("api_key_missing", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ Assert.Equal(ErrorType.General, res.Result.ErrorType);
+ }
+
+ [Fact]
+ public async Task should_have_unauthorized_if_invalid_token()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond),
+ ApiKey = "ff877c7a-4594-43b5-89a8-df44c9984bd8"
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("invalid_api_key", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ Assert.Equal(ErrorType.General, res.Result.ErrorType);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_flag_does_not_exists()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("flag_not_found", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_we_expect_a_boolean_and_got_another_type()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("string_key", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_boolean_flag_with_TARGETING_MATCH_reason()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("bool_targeting_match", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.True(res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_return_custom_reason_if_returned_by_relay_proxy()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("unknown_reason", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.True(res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal("CUSTOM_REASON", res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_boolean_default_value_if_the_flag_is_disabled()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetBooleanDetailsAsync("disabled", false, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.False(res.Result.Value);
+ Assert.Equal(Reason.Disabled, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_type()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetStringDetailsAsync("bool_targeting_match", "default", _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal("default", res.Result.Value);
+ Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetStringDetailsAsync("string_key", "defaultValue", _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal("CC0000", res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_string_default_value_if_the_flag_is_disabled()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetStringDetailsAsync("disabled_string", "defaultValue", _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal("defaultValue", res.Result.Value);
+ Assert.Equal(Reason.Disabled, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_we_expect_a_integer_and_got_another_type()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetIntegerDetailsAsync("string_key", 200, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(200, res.Result.Value);
+ Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_integer_flag_with_TARGETING_MATCH_reason()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetIntegerDetailsAsync("integer_key", 1200, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(100, res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_integer_default_value_if_the_flag_is_disabled()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(1225, res.Result.Value);
+ Assert.Equal(Reason.Disabled, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_throw_an_error_if_we_expect_a_integer_and_double_type()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetIntegerDetailsAsync("double_key", 200, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(200, res.Result.Value);
+ Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_double_flag_with_TARGETING_MATCH_reason()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetDoubleDetailsAsync("double_key", 1200.25, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(100.25, res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_double_default_value_if_the_flag_is_disabled()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetDoubleDetailsAsync("disabled_double", 1225.34, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(1225.34, res.Result.Value);
+ Assert.Equal(Reason.Disabled, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_value_flag_with_TARGETING_MATCH_reason()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("object_key", null, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ var want = JsonSerializer.Serialize(new Value(new Structure(new Dictionary
+ {
+ { "test", new Value("test1") }, { "test2", new Value(false) }, { "test3", new Value(123.3) },
+ { "test4", new Value(1) }
+ })));
+ Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value));
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_wrap_into_value_if_wrong_type()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("string_key", null, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(new Value("CC0000").AsString, res.Result.Value.AsString);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_object_default_value_if_the_flag_is_disabled()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("disabled_object", new Value("default"), _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(new Value("default").AsString, res.Result.Value.AsString);
+ Assert.Equal(Reason.Disabled, res.Result.Reason);
+ }
+
+
+ [Fact]
+ public async Task should_throw_an_error_if_no_targeting_key()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetStringDetailsAsync("list_key", "empty", EvaluationContext.Empty);
+ Assert.NotNull(res.Result);
+ Assert.Equal("empty", res.Result.Value);
+ Assert.Equal(ErrorType.InvalidContext, res.Result.ErrorType);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ }
+
+ [Fact]
+ public async Task should_resolve_a_valid_value_flag_with_a_list()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("list_key", null, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ var want = JsonSerializer.Serialize(new Value(new List
+ { new("test"), new("test1"), new("test2"), new("false"), new("test3") }));
+ Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value));
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ }
+
+ [Fact]
+ public async Task should_use_object_default_value_if_flag_not_found()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("does_not_exists", new Value("default"), _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(new Value("default").AsString, res.Result.Value.AsString);
+ Assert.Equal(Reason.Error, res.Result.Reason);
+ Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType);
+ Assert.Equal("flag does_not_exists was not found in your configuration", res.Result.ErrorMessage);
+ }
+
+ [Fact]
+ public async Task should_have_default_exporter_metadata_in_context()
+ {
+ string capturedRequestBody = null;
+ var mock = new MockHttpMessageHandler();
+ var mockedRequest = mock.When($"{prefixEval}integer_key").Respond(
+ async request =>
+ {
+ capturedRequestBody = await request.Content.ReadAsStringAsync();
+ return new HttpResponseMessage
+ {
+ Content = new StringContent(
+ "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"
+ , Encoding.UTF8, "application/json")
+ };
+ });
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = mock,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("integer_key", new Value("default"), _defaultEvaluationCtx);
+ Assert.Equal(1, mock.GetMatchCount(mockedRequest));
+ await Task.Delay(100); // time to wait to be sure body is extracted
+ var want = JObject.Parse(
+ "{\"context\":{\"labels\":[\"pro\",\"beta\"],\"gofeatureflag\":{\"openfeature\":true,\"provider\":\".NET\"},\"age\":30,\"firstname\":\"john\",\"professional\":true,\"company_info\":{\"name\":\"my_company\",\"size\":120},\"lastname\":\"doe\",\"anonymous\":false,\"rate\":3.14,\"email\":\"john.doe@gofeatureflag.org\",\"targetingKey\":\"d45e303a-38c2-11ed-a261-0242ac120002\"}}");
+ var got = JObject.Parse(capturedRequestBody);
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public async Task should_have_custom_exporter_metadata_in_context()
+ {
+ string capturedRequestBody = null;
+ var mock = new MockHttpMessageHandler();
+ var mockedRequest = mock.When($"{prefixEval}integer_key").Respond(
+ async request =>
+ {
+ capturedRequestBody = await request.Content.ReadAsStringAsync();
+ return new HttpResponseMessage
+ {
+ Content = new StringContent(
+ "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"
+ , Encoding.UTF8, "application/json")
+ };
+ });
+ var exporterMetadata = new ExporterMetadata();
+ exporterMetadata.Add("key1", "value1");
+ exporterMetadata.Add("key2", 1.234);
+ exporterMetadata.Add("key3", 10);
+ exporterMetadata.Add("key4", false);
+
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = mock,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond),
+ ExporterMetadata = exporterMetadata
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetObjectDetailsAsync("integer_key", new Value("default"), _defaultEvaluationCtx);
+ Assert.Equal(1, mock.GetMatchCount(mockedRequest));
+ await Task.Delay(100); // time to wait to be sure body is extracted
+ var want = JObject.Parse(
+ "{\"context\":{\"labels\":[\"pro\",\"beta\"],\"gofeatureflag\":{\"openfeature\":true,\"provider\":\".NET\",\"key1\":\"value1\",\"key2\":1.234,\"key3\":10,\"key4\":false},\"age\":30,\"firstname\":\"john\",\"professional\":true,\"company_info\":{\"name\":\"my_company\",\"size\":120},\"lastname\":\"doe\",\"anonymous\":false,\"rate\":3.14,\"email\":\"john.doe@gofeatureflag.org\",\"targetingKey\":\"d45e303a-38c2-11ed-a261-0242ac120002\"}}");
+ var got = JObject.Parse(capturedRequestBody);
+
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public async Task should_resolve_a_flag_with_metadata()
+ {
+ var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
+ {
+ Endpoint = baseUrl,
+ HttpMessageHandler = _mockHttp,
+ Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
+ });
+ await Api.Instance.SetProviderAsync(g);
+ var client = Api.Instance.GetClient("test-client");
+ var res = client.GetIntegerDetailsAsync("integer_with_metadata", 1200, _defaultEvaluationCtx);
+ Assert.NotNull(res.Result);
+ Assert.Equal(100, res.Result.Value);
+ Assert.Equal(ErrorType.None, res.Result.ErrorType);
+ Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
+ Assert.Equal("True", res.Result.Variant);
+ Assert.NotNull(res.Result.FlagMetadata);
+ Assert.Equal("key1", res.Result.FlagMetadata.GetString("key1"));
+ Assert.Equal(1, res.Result.FlagMetadata.GetInt("key2"));
+ Assert.Equal(1.345, res.Result.FlagMetadata.GetDouble("key3"));
+ Assert.True(res.Result.FlagMetadata.GetBool("key4"));
+ }
+}
diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs
deleted file mode 100644
index 78767989..00000000
--- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.Text.Json;
-using OpenFeature.Model;
-using Xunit;
-
-namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
-
-public class GoFeatureFlagUserTest
-{
- [Fact]
- public void GoFeatureFlagUserSerializesCorrectly()
- {
- var userContext = EvaluationContext.Builder()
- .Set("targetingKey", "1d1b9238-2591-4a47-94cf-d2bc080892f1")
- .Set("firstname", "john")
- .Set("lastname", "doe")
- .Set("email", "john.doe@gofeatureflag.org")
- .Set("admin", true)
- .Set("anonymous", false)
- .Build();
-
- GoFeatureFlagUser user = userContext;
-
- var userAsString = JsonSerializer.Serialize(user,
- new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
-
- Assert.Contains("{\"key\":\"1d1b9238-2591-4a47-94cf-d2bc080892f1\",\"anonymous\":false,\"custom\":{",
- userAsString);
- }
-}
\ No newline at end of file
diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs
new file mode 100644
index 00000000..6672e121
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Newtonsoft.Json.Linq;
+using OpenFeature.Contrib.Providers.GOFeatureFlag.converters;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test;
+
+public class OfrepSerializationTest
+{
+ [Fact]
+ public void OfrepSerializesCorrectly()
+ {
+ var ctx = EvaluationContext.Builder()
+ .Set("targetingKey", "1d1b9238-2591-4a47-94cf-d2bc080892f1")
+ .Set("firstname", "john")
+ .Set("lastname", "doe")
+ .Set("email", "john.doe@gofeatureflag.org")
+ .Set("admin", true)
+ .Set("anonymous", false)
+ .Build();
+
+ var ofrepReq = new OfrepRequest(ctx);
+
+ var want = JObject.Parse("{\"context\":{\"firstname\":\"john\",\"email\":\"john.doe@gofeatureflag.org\",\"lastname\":\"doe\",\"targetingKey\":\"1d1b9238-2591-4a47-94cf-d2bc080892f1\",\"admin\":true,\"anonymous\":false}}");
+ var got = JObject.Parse(ofrepReq.AsJsonString());
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary()
+ {
+ var evaluationContext = EvaluationContext.Builder().Build();
+ var want = JObject.Parse("{\"context\":{}}");
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContext_ShouldReturnADictionaryWithValues()
+ {
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("location", "somewhere")
+ .Build();
+
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"location\":\"somewhere\",\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndIntegerValue_ShouldReturnADictionaryWithStringValues()
+ {
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("age", 23)
+ .Build();
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"age\":23,\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndValuesOfStrings_ShouldReturnADictionaryWithSerializedStringValues()
+ {
+ var testStructure = new Structure(new Dictionary
+ {
+ { "config1", new Value("value1") },
+ { "config2", new Value("value2") }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("config", testStructure)
+ .Build();
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"config\":{\"config1\":\"value1\", \"config2\":\"value2\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextAndMixedValueTypes_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var dateTime = new DateTime(2025, 9, 1);
+ var testStructure = new Structure(new Dictionary
+ {
+ { "config1", new Value(1) },
+ { "config2", new Value("value2") },
+ { "config3", new Value(dateTime) }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("config", testStructure)
+ .Build();
+
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"config\":{\"config3\":\"2025-09-01T00:00:00\",\"config2\":\"value2\",\"config1\":1},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var sampleDictionary = new Dictionary();
+ sampleDictionary["config2"] = new Value([
+ new Value([new Value("element1-1"), new Value("element1-2")]),
+ new Value("element2"),
+ new Value("element3")
+ ]);
+ sampleDictionary["config3"] = new Value(new DateTime(2025, 9, 1));
+
+ var testStructure = new Structure(sampleDictionary);
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("config", testStructure)
+ .Build();
+
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"config\":{\"config2\":[[\"element1-1\",\"element1-2\"],\"element2\",\"element3\"],\"config3\":\"2025-09-01T00:00:00\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+
+ [Fact]
+ public void ToStringDictionary_WithContextWithNestedStructure_ShouldReturnADictionaryWithSerializedValues()
+ {
+ var testStructure = new Structure(new Dictionary
+ {
+ {
+ "config-value-struct", new Value(new Structure(new Dictionary
+ {
+ { "nested1", new Value(1) }
+ }))
+ },
+ { "config-value-value", new Value(new Value(new DateTime(2025, 9, 1))) }
+ });
+
+ var evaluationContext = EvaluationContext.Builder()
+ .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67")
+ .Set("config", testStructure)
+ .Build();
+ var request = new Dictionary { { "context", evaluationContext.AsDictionary() } };
+ var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings));
+ var want = JObject.Parse("{\"context\":{\"config\":{\"config-value-struct\":{\"nested1\":1},\"config-value-value\":\"2025-09-01T00:00:00\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}");
+ Assert.True(JToken.DeepEquals(want, got), "unexpected json");
+ }
+}
diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj
index 96b51275..c29bafd5 100644
--- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj
+++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj
@@ -6,6 +6,7 @@
-
+
+