Compare commits

...

33 Commits
v0.3.2 ... main

Author SHA1 Message Date
renovate[bot] 2003666eaa
chore(deps): update dependency openfeature to 2.7.0 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 17:09:14 +00:00
renovate[bot] cf962d967c
fix(deps): update module dagger.io/dagger to v0.18.12 (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 19:12:21 +00:00
renovate[bot] 2835e3cf10
fix(deps): update module dagger.io/dagger to v0.18.11 (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 16:56:43 +00:00
Brianna Bland 288023c5dd
feat: basic object flags (#141)
Signed-off-by: bblandSigned-off-by: bbland1 <104288486+bbland1@users.noreply.github.com>
2025-06-22 10:34:35 -04:00
Aditya Kumar b867485101
feat: integration tests for nodejs generator (#140)
* feat: add renovate.json file #122

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* chore: remove packageRules from renovate.json

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* feat(nodejs-integration): add Node.js integration test and related files

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* feat: integration tests for nodejs generator (#117)

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* fix: fix whitespace issue in Makefile

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* fix: fix whitespace issue in Makefile

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* fix: fix whitespaces in Makefile

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

---------

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>
2025-06-13 18:39:52 +00:00
Gayatri Chakkithara 6145c2e15d
test: add Go integration test (#125)
* go integration test

Signed-off-by: redpinecube <tara.chakkithara@icloud.com>

* fixed go integration test

Signed-off-by: redpinecube <tara.chakkithara@icloud.com>

* Update test/go-integration/test.go

Co-authored-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Signed-off-by: Gayatri Chakkithara <113033661+redpinecube@users.noreply.github.com>

* Update test/go-integration/test.go

Co-authored-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Signed-off-by: Gayatri Chakkithara <113033661+redpinecube@users.noreply.github.com>

* Update test/go-integration/test.go

Co-authored-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Signed-off-by: Gayatri Chakkithara <113033661+redpinecube@users.noreply.github.com>

* cleanup the test

Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com>

---------

Signed-off-by: redpinecube <tara.chakkithara@icloud.com>
Signed-off-by: Gayatri Chakkithara <113033661+redpinecube@users.noreply.github.com>
Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Co-authored-by: Roman Dmytrenko <rdmytrenko@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-06-12 16:00:46 +00:00
Michael Beemer 32f912a089
chore: add merge group trigger to pr lint
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-06-11 16:09:06 -04:00
Michael Beemer 594cf538be
chore: revert golang ci lint to v6
Reverts 4f909c1068

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-06-11 16:08:18 -04:00
renovate[bot] 5d7a60754a
chore(deps): update dependency microsoft.extensions.dependencyinjection to 9.0.6 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 22:36:59 +00:00
renovate[bot] 8b70612472
fix(deps): update module dagger.io/dagger to v0.18.10 (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:50:33 +00:00
renovate[bot] 51232fea1c
chore(deps): update dependency microsoft.extensions.dependencyinjection to v9 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 14:01:49 +00:00
renovate[bot] 4f909c1068
chore(deps): update golangci/golangci-lint-action action to v8 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 02:46:27 +00:00
renovate[bot] 1b4d062045
chore(deps): update dagger/dagger-for-github action to v7 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 21:48:46 +00:00
renovate[bot] 35533dceb0
chore(deps): update dependency openfeature to 2.6.0 (#131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 21:48:40 +00:00
renovate[bot] a25f90a65b
fix(deps): update module github.com/pterm/pterm to v0.12.81 (#129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:57:32 +00:00
renovate[bot] 11b9c34d86
chore(deps): update alpine docker tag to v3.22 (#130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:57:17 +00:00
renovate[bot] 3ec8443823
chore(deps): update dependency microsoft.extensions.dependencyinjection to 8.0.1 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 16:06:18 +00:00
Aditya Kumar 83dac705b9
chore: add renovate.json file #122 (#124)
* feat: add renovate.json file #122

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

* chore: remove packageRules from renovate.json

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>

---------

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>
2025-06-07 16:04:05 +00:00
Sahid Velji bcd11ea9c8
chore: fix the directory structure (#121)
* chore: fix the directory structure

Signed-off-by: Sahid Velji <sahidvelji@gmail.com>

* chore: upgrade dependencies (#123)

Signed-off-by: Sahid Velji <sahidvelji@gmail.com>

---------

Signed-off-by: Sahid Velji <sahidvelji@gmail.com>
2025-05-31 18:13:14 +00:00
Sahid Velji 79d3dceb3a
chore: upgrade dependencies (#123)
Signed-off-by: Sahid Velji <sahid.velji@capitalone.com>
Co-authored-by: Sahid Velji <sahid.velji@capitalone.com>
2025-05-31 12:09:41 +00:00
Aditya Kumar 8eec77965a
feat(flagset): improve validation error formatting in Load function (#119)
feat(flagset): improve validation error formatting in Load function #110

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>
2025-05-30 16:24:09 +00:00
OpenFeature Bot eedccd606f
chore(main): release 0.3.5 (#112)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-05-20 19:28:52 +00:00
Simon Schrottner 49e65c8283
fix: Naming of generated java class (#111)
Java expects to have a class with the same name as the file within the file. This constraint was violated, and renaming the class ort the file was needed
2025-05-20 15:19:37 -04:00
OpenFeature Bot c1466b48e2
chore(main): release 0.3.4 (#108)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-05-14 19:35:51 +00:00
Kris Coleman 063cfca2d7
feat: adds compare command (#93)
* feat: add compare command to cli

A new 'compare' command has been added to the CLI. This command allows users to compare two manifest files and list the changes between them. The comparison is done by loading each manifest file, then using a new function from the 'manifest' package to identify differences. Error handling has also been implemented for scenarios where loading or comparing manifests fails. Tests were also included to test the compare function.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat: adds tree behavior to compare cmd

Compare now supports flat, tree, and color output modes.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* chore: refactors compare command flag handling

Refactors the compare command to use the global manifest
flag and introduces an 'against' flag for specifying the
target manifest for comparison.

This change simplifies command usage and aligns it with
other commands that utilize the global manifest configuration.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat: Adds output formats to compare

Adds the ability to render the differences between manifests in JSON format,
providing a structured output suitable for consumption by other tools or
systems. Also, introduces validation for the output format flag.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat: adds yaml output format

Adds the ability to output manifest comparison results in YAML format.

This provides users with another option for viewing and processing
the differences between manifests, which can be useful for
configuration management and automation tasks.

The changes include:
- Implementation of a new render function for YAML output.
- Modification of the output format selection logic to include YAML.
- Addition of YAML as a valid output format option in the command
  line interface and documentation.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

---------

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
2025-05-14 19:27:46 +00:00
Advitiya Jain 9a9f11fc6c
feat: add java generator (#107)
* Add Java generator

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Add Readme file for java

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Fix command description to show same value as openfeature_generate_java.md

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Change package name to com.example.openfeature

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Fix variable type and imports according to Openfeature java-SDK

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Refactor: expose only interface via factory method and hide client implementation

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

* Update generated docs after java generator extension

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>

---------

Signed-off-by: advitiya0201 <f20210979@pilani.bits-pilani.ac.in>
2025-05-12 20:29:06 +00:00
Kris Coleman 96f4cde0f8
feat: introduce dagger for integration testing and ci (#100)
* feat: introduce dagger for integration testing and ci

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat: make integration testing extensible and modular

my intention here is to make the pattern for integration testing more accessible.
let's abstract the integration test pattern itself into a framework to harness the boiler plate.
then reuse our csharp integration test code as our first integration-test.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

---------

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
2025-05-06 18:08:32 +00:00
OpenFeature Bot 230956e6b4
chore(main): release 0.3.3 (#98)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-04-18 14:31:00 +00:00
Lukas Reining 5210429e39
feat: add codegen for NestJS (#99)
Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-04-18 14:28:35 +00:00
Kris Coleman e32547f734
chore: automate project standards before push (#94)
feat: integrates pre-commit and pre-push hooks to automate standards

Adds Lefthook configuration to automate Git hooks, ensuring consistent pre-commit & pre-push checks.

Lefthook automates pre-commit checks such as code formatting and pre-push checks to ensure documentation and tests are up-to-date.

This helps maintain code quality and consistency across the team. I find this saves me time as I switch projects and standards, so I don't push and wait for a PR check to fail.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
2025-04-14 17:58:40 +00:00
Kris Coleman ae645813c4
feat(csharp): added generator and integration tests (#97)
* feat: adds c# generator

Adds a new generator for C# to create typesafe clients.
This allows users to generate C# code based on feature flag
definitions, streamlining integration with .NET applications.
Includes necessary command-line flags, templates, and tests.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat(csharp): adds C# generator integration test

Adds a C# code generator integration test to ensure the generated C# code compiles correctly.

This includes:
- A new C# generator based on templates
- Updates to the build process and documentation to support C# generation and testing
- An integration test using Docker to compile the generated C# code
- Fixes and adjustments to data type mappings for C# compatibility

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* chore: go fmt fixes

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* Update .github/workflows/csharp-integration.yml

Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>
Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* Update CONTRIBUTING.md

Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>
Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* Update internal/generators/csharp/csharp.go

Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>
Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* chore(ci): moved the csharp integration into pr-test workflow as a separate job

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* chore: cleaned up generate code to private funcs are private

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* feat(csharp): implemented di for generated code

- updated openfeature to 2.3.2
- introduced IServiceCollection and DI patterns
- updated tests and expectations

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>

* Update .github/workflows/pr-test.yml

Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>

* Update .github/workflows/pr-test.yml

Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>

---------

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
Signed-off-by: Kris Coleman <kris.blacksuitmedia@gmail.com>
Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-04-14 17:40:52 +00:00
Kim Gustyr 1f8f43ae04
feat: Python generator (#95)
* feat: Python generator

Signed-off-by: Kim Gustyr <kim.gustyr@flagsmith.com>

* fix booleans

Signed-off-by: Kim Gustyr <kim.gustyr@flagsmith.com>

* fix typo

Signed-off-by: Kim Gustyr <kim.gustyr@flagsmith.com>

* add docs

Signed-off-by: Kim Gustyr <kim.gustyr@flagsmith.com>

---------

Signed-off-by: Kim Gustyr <kim.gustyr@flagsmith.com>
2025-04-10 12:37:44 +00:00
Roman Dmytrenko 412a1174b5
fix: use the correct json schema url in init command (#96)
Signed-off-by: Roman Dmytrenko <rdmytrenko@gmail.com>
2025-04-07 12:56:39 +00:00
91 changed files with 6343 additions and 422 deletions

View File

@ -4,6 +4,7 @@ on:
branches:
- main
pull_request:
merge_group:
permissions:
# Required: allow read access to the content for analysis.

View File

@ -44,3 +44,22 @@ jobs:
git diff
exit 1
fi
integration-tests:
name: 'Generator Integration Tests'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Run all integration tests with Dagger
uses: dagger/dagger-for-github@b81317a976cb7f7125469707321849737cd1b3bc # v7
with:
workdir: .
verb: run
args: go run ./test/integration/cmd/run.go
version: 'latest'

7
.gitignore vendored
View File

@ -27,3 +27,10 @@ dist
# openfeature cli config
.openfeature.yaml
.idea/
node_modules/
npm-debug.log*
generated/
*.log

View File

@ -21,6 +21,7 @@ builds:
- linux
- windows
- darwin
binary: ./cmd/openfeature
archives:
- formats: tar.gz
@ -35,49 +36,49 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
formats: [ 'zip' ]
formats: ["zip"]
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
report_sizes: true
dockers:
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/amd64
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.description="OpenFeatures official command-line tool"
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=Apache-2.0
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/amd64
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.description="OpenFeatures official command-line tool"
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=Apache-2.0
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/arm64
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.description="OpenFeatures official command-line tool"
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=Apache-2.0
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/arm64
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.description="OpenFeatures official command-line tool"
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=Apache-2.0
docker_manifests:
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
image_templates:
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
- name_template: ghcr.io/open-feature/cli:latest
image_templates:
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
image_templates:
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
- name_template: ghcr.io/open-feature/cli:latest
image_templates:
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
- ghcr.io/open-feature/cli:{{ .Version }}-arm64

View File

@ -1,3 +1,3 @@
{
".": "0.3.2"
".": "0.3.5"
}

View File

@ -1,5 +1,40 @@
# Changelog
## [0.3.5](https://github.com/open-feature/cli/compare/v0.3.4...v0.3.5) (2025-05-20)
### 🐛 Bug Fixes
* Naming of generated java class ([#111](https://github.com/open-feature/cli/issues/111)) ([49e65c8](https://github.com/open-feature/cli/commit/49e65c828330abb732eb3b9cf85850bb5ac36531))
## [0.3.4](https://github.com/open-feature/cli/compare/v0.3.3...v0.3.4) (2025-05-14)
### ✨ New Features
* add java generator ([#107](https://github.com/open-feature/cli/issues/107)) ([9a9f11f](https://github.com/open-feature/cli/commit/9a9f11fc6c6a8ffa38870e62ac26d9f8f679825b))
* adds compare command ([#93](https://github.com/open-feature/cli/issues/93)) ([063cfca](https://github.com/open-feature/cli/commit/063cfca2d79c9f75e181422ec375e300e020e57f))
* introduce dagger for integration testing and ci ([#100](https://github.com/open-feature/cli/issues/100)) ([96f4cde](https://github.com/open-feature/cli/commit/96f4cde0f87b8daf70e02c1d4ca3bcec018fee02))
## [0.3.3](https://github.com/open-feature/cli/compare/v0.3.2...v0.3.3) (2025-04-18)
### 🐛 Bug Fixes
* use the correct json schema url in init command ([#96](https://github.com/open-feature/cli/issues/96)) ([412a117](https://github.com/open-feature/cli/commit/412a1174b5dfe9ba77e18ec57d5a761711067386))
### ✨ New Features
* add codegen for NestJS ([#99](https://github.com/open-feature/cli/issues/99)) ([5210429](https://github.com/open-feature/cli/commit/5210429e39c10c91482cb0a0a8b2f4431a0aa182))
* **csharp:** added generator and integration tests ([#97](https://github.com/open-feature/cli/issues/97)) ([ae64581](https://github.com/open-feature/cli/commit/ae645813c48b5ef10d8557406e7ab5c96ce3df69))
* Python generator ([#95](https://github.com/open-feature/cli/issues/95)) ([1f8f43a](https://github.com/open-feature/cli/commit/1f8f43ae049fcf7c4feba3edaa697329688f7343))
### 🧹 Chore
* automate project standards before push ([#94](https://github.com/open-feature/cli/issues/94)) ([e32547f](https://github.com/open-feature/cli/commit/e32547f73495a525ed4ef5e2cadd45642d6fb172))
## [0.3.2](https://github.com/open-feature/cli/compare/v0.3.1...v0.3.2) (2025-04-02)

View File

@ -1,3 +1,7 @@
# Contributing to OpenFeature CLI
Thank you for your interest in contributing to the OpenFeature CLI! This document provides guidelines and instructions to help you get started with contributing to the project. Whether you're fixing a bug, adding a new feature, or improving documentation, your contributions are greatly appreciated.
## Contributing New Generators
We welcome contributions for new generators to extend the functionality of the OpenFeature CLI. Below are the steps to contribute a new generator:
@ -32,6 +36,81 @@ We welcome contributions for new generators to extend the functionality of the O
11. **Address Feedback**: Be responsive to feedback from the maintainers. Make any necessary changes and update your pull request as needed.
### Testing
The OpenFeature CLI includes both unit and integration tests to ensure quality and correctness.
#### Unit Tests
Run the unit tests with:
```bash
go test ./...
```
#### Integration Tests
To verify that generated code compiles correctly, the project includes integration tests. The CLI uses a Dagger-based integration testing framework to test code generation for each supported language:
```bash
# Run all integration tests
make test-integration
# Run tests for a specific language
make test-csharp-dagger
```
For more information on the integration testing framework, see [Integration Testing](./docs/integration-testing.md).
## Setting Up Lefthook
To streamline the setup of Git hooks for this project, we utilize [Lefthook](https://github.com/evilmartians/lefthook). Lefthook automates pre-commit and pre-push checks, ensuring consistent enforcement of best practices across the team. These checks include code formatting, documentation generation, and running tests.
This tool is particularly helpful for new contributors or those returning to the project after some time, as it provides a seamless way to align with the project's standards. By catching issues early in your local development environment, Lefthook helps you address potential problems before opening a Pull Request, saving time and effort for both contributors and maintainers.
### Installation
1. Install Lefthook using Homebrew:
```bash
brew install lefthook
```
2. Install the Lefthook configuration into your Git repository:
```bash
lefthook install
```
### Pre-Commit Hook
The pre-commit hook is configured to run the following check:
1. **Code Formatting**: Ensures all files are properly formatted using `go fmt`. Any changes made by `go fmt` will be automatically staged.
### Pre-Push Hook
The pre-push hook is configured to run the following checks:
1. **Documentation Generation**: Runs `make generate-docs` to ensure documentation is up-to-date. If any changes are detected, the push will be blocked until the changes are committed.
2. **Tests**: Executes `make test` to verify that all tests pass. If any tests fail, the push will be blocked.
### Running Hooks Manually
You can manually run the hooks using the following commands:
- Pre-commit hook:
```bash
lefthook run pre-commit
```
- Pre-push hook:
```bash
lefthook run pre-push
```
## Templates
### Data

View File

@ -1,4 +1,4 @@
FROM alpine:3.21
FROM alpine:3.22
COPY ./openfeature usr/local/bin/openfeature

View File

@ -1,10 +1,30 @@
.PHONY: test
test:
@echo "Running tests..."
@go test -v ./...
@echo "Tests passed successfully!"
# Dagger-based integration tests
.PHONY: test-integration-csharp
test-integration-csharp:
@echo "Running C# integration test with Dagger..."
@go run ./test/integration/cmd/csharp/run.go
.PHONY: test-integration-go
test-integration-go:
@echo "Running Go integration test with Dagger..."
@go run ./test/integration/cmd/go/run.go
.PHONY: test-integration-nodejs
test-integration-nodejs:
@echo "Running NodeJS integration test with Dagger..."
@go run ./test/integration/cmd/nodejs/run.go
.PHONY: test-integration
test-integration:
@echo "Running all integration tests with Dagger..."
@go run ./test/integration/cmd/run.go
generate-docs:
@echo "Generating documentation..."
@go run ./docs/generate-commands.go
@ -14,3 +34,9 @@ generate-schema:
@echo "Generating schema..."
@go run ./schema/generate-schema.go
@echo "Schema generated successfully!"
.PHONY: fmt
fmt:
@echo "Running go fmt..."
@go fmt ./...
@echo "Code formatted successfully!"

View File

@ -58,7 +58,7 @@ docker run -it -v $(pwd):/local -w /local ghcr.io/open-feature/cli:latest
If you have `Go >= 1.23` installed, you can install the CLI using the following command:
```bash
go install github.com/open-feature/cli@latest
go install github.com/open-feature/cli/cmd/openfeature@latest
```
### via pre-built binaries

View File

@ -1,209 +0,0 @@
package cmd
import (
"strings"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"github.com/open-feature/cli/internal/generators/golang"
"github.com/open-feature/cli/internal/generators/nodejs"
"github.com/open-feature/cli/internal/generators/react"
"github.com/open-feature/cli/internal/logger"
"github.com/spf13/cobra"
)
// addStabilityInfo adds stability information to the command's help template before "Usage:"
func addStabilityInfo(cmd *cobra.Command) {
// Only modify commands that have a stability annotation
if stability, ok := cmd.Annotations["stability"]; ok {
originalTemplate := cmd.UsageTemplate()
// Find the "Usage:" section and insert stability info before it
if strings.Contains(originalTemplate, "Usage:") {
customTemplate := strings.Replace(
originalTemplate,
"Usage:",
"Stability: "+stability+"\n\nUsage:",
1, // Replace only the first occurrence
)
cmd.SetUsageTemplate(customTemplate)
} else {
// Fallback if "Usage:" not found - prepend to the template
customTemplate := "Stability: " + stability + "\n\n" + originalTemplate
cmd.SetUsageTemplate(customTemplate)
}
}
}
func GetGenerateNodeJSCmd() *cobra.Command {
nodeJSCmd := &cobra.Command{
Use: "nodejs",
Short: "Generate typesafe Node.js client.",
Long: `Generate typesafe Node.js client compatible with the OpenFeature JavaScript Server SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.nodejs")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Node.js")
params := generators.Params[nodejs.Params]{
OutputPath: outputPath,
Custom: nodejs.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := nodejs.NewGenerator(flagset)
logger.Default.Debug("Executing Node.js generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Node.js")
return nil
},
}
addStabilityInfo(nodeJSCmd)
return nodeJSCmd
}
func GetGenerateReactCmd() *cobra.Command {
reactCmd := &cobra.Command{
Use: "react",
Short: "Generate typesafe React Hooks.",
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.react")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("React")
params := generators.Params[react.Params]{
OutputPath: outputPath,
Custom: react.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := react.NewGenerator(flagset)
logger.Default.Debug("Executing React generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("React")
return nil
},
}
addStabilityInfo(reactCmd)
return reactCmd
}
func GetGenerateGoCmd() *cobra.Command {
goCmd := &cobra.Command{
Use: "go",
Short: "Generate typesafe accessors for OpenFeature.",
Long: `Generate typesafe accessors compatible with the OpenFeature Go SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.go")
},
RunE: func(cmd *cobra.Command, args []string) error {
goPackageName := config.GetGoPackageName(cmd)
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Go")
params := generators.Params[golang.Params]{
OutputPath: outputPath,
Custom: golang.Params{
GoPackage: goPackageName,
},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := golang.NewGenerator(flagset)
logger.Default.Debug("Executing Go generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Go")
return nil
},
}
// Add Go-specific flags
config.AddGoGenerateFlags(goCmd)
addStabilityInfo(goCmd)
return goCmd
}
func init() {
// Register generators with the manager
generators.DefaultManager.Register(GetGenerateReactCmd)
generators.DefaultManager.Register(GetGenerateGoCmd)
generators.DefaultManager.Register(GetGenerateNodeJSCmd)
}
func GetGenerateCmd() *cobra.Command {
generateCmd := &cobra.Command{
Use: "generate",
Short: "Generate typesafe OpenFeature accessors.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate")
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("Available generators:")
return generators.DefaultManager.PrintGeneratorsTable()
},
}
// Add generate flags using the config package
config.AddGenerateFlags(generateCmd)
// Add all registered generator commands
for _, subCmd := range generators.DefaultManager.GetCommands() {
generateCmd.AddCommand(subCmd)
}
addStabilityInfo(generateCmd)
return generateCmd
}

View File

@ -1,6 +1,6 @@
package main
import "github.com/open-feature/cli/cmd"
import "github.com/open-feature/cli/internal/cmd"
var (
// Overridden by Go Releaser at build time

View File

@ -23,6 +23,7 @@ openfeature [flags]
### SEE ALSO
* [openfeature compare](openfeature_compare.md) - Compare two feature flag manifests
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
* [openfeature init](openfeature_init.md) - Initialize a new project
* [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI

View File

@ -0,0 +1,34 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature compare
Compare two feature flag manifests
### Synopsis
Compare two OpenFeature flag manifests and display the differences in a structured format.
```
openfeature compare [flags]
```
### Options
```
-a, --against string Path to the target manifest file to compare against
-h, --help help for compare
-o, --output string Output format. Valid formats: tree, flat, json, yaml (default "tree")
```
### Options inherited from parent commands
```
--debug Enable debug logging
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.

View File

@ -26,7 +26,11 @@ openfeature generate [flags]
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.
* [openfeature generate csharp](openfeature_generate_csharp.md) - Generate typesafe C# client.
* [openfeature generate go](openfeature_generate_go.md) - Generate typesafe accessors for OpenFeature.
* [openfeature generate java](openfeature_generate_java.md) - Generate typesafe Java client.
* [openfeature generate nestjs](openfeature_generate_nestjs.md) - Generate typesafe NestJS decorators.
* [openfeature generate nodejs](openfeature_generate_nodejs.md) - Generate typesafe Node.js client.
* [openfeature generate python](openfeature_generate_python.md) - Generate typesafe Python client.
* [openfeature generate react](openfeature_generate_react.md) - Generate typesafe React Hooks.

View File

@ -0,0 +1,37 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate csharp
Generate typesafe C# client.
> **Stability**: alpha
### Synopsis
Generate typesafe C# client compatible with the OpenFeature .NET SDK.
```
openfeature generate csharp [flags]
```
### Options
```
-h, --help help for csharp
--namespace string Namespace for the generated C# code (default "OpenFeature")
```
### Options inherited from parent commands
```
--debug Enable debug logging
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -0,0 +1,37 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate java
Generate typesafe Java client.
> **Stability**: alpha
### Synopsis
Generate typesafe Java client compatible with the OpenFeature Java SDK.
```
openfeature generate java [flags]
```
### Options
```
-h, --help help for java
--package-name string Name of the generated Java package (default "com.example.openfeature")
```
### Options inherited from parent commands
```
--debug Enable debug logging
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -0,0 +1,36 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate nestjs
Generate typesafe NestJS decorators.
> **Stability**: alpha
### Synopsis
Generate typesafe NestJS decorators compatible with the OpenFeature NestJS SDK.
```
openfeature generate nestjs [flags]
```
### Options
```
-h, --help help for nestjs
```
### Options inherited from parent commands
```
--debug Enable debug logging
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -0,0 +1,36 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate python
Generate typesafe Python client.
> **Stability**: alpha
### Synopsis
Generate typesafe Python client compatible with the OpenFeature Python SDK.
```
openfeature generate python [flags]
```
### Options
```
-h, --help help for python
```
### Options inherited from parent commands
```
--debug Enable debug logging
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -6,7 +6,7 @@ import (
"regexp"
"strings"
"github.com/open-feature/cli/cmd"
"github.com/open-feature/cli/internal/cmd"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)

70
go.mod
View File

@ -2,53 +2,85 @@ module github.com/open-feature/cli
go 1.23.0
toolchain go1.24.0
require (
github.com/google/go-cmp v0.6.0
dagger.io/dagger v0.18.12
github.com/google/go-cmp v0.7.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.13.0
github.com/pterm/pterm v0.12.80
github.com/pterm/pterm v0.12.81
github.com/spf13/afero v1.14.0
github.com/spf13/cobra v1.8.1
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.0
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/text v0.23.0
golang.org/x/text v0.26.0
gopkg.in/yaml.v3 v3.0.1
)
require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/99designs/gqlgen v0.17.75 // indirect
github.com/Khan/genqlient v0.8.1 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.8.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.28 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/otel/log v0.12.2 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

161
go.sum
View File

@ -6,6 +6,18 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
dagger.io/dagger v0.18.10 h1:Ibyz5LqxjjEHfLMlaU9PJ3xt3ju7p29RWy0lVfvSNU0=
dagger.io/dagger v0.18.10/go.mod h1:VSj+2HMd/EnaCVt7gTY70p8LBW+oQDYjA1XTadr8vBE=
dagger.io/dagger v0.18.11 h1:6lSfemlbGM2HmdOjhgevrX2+orMDGKU/xTaBMZ+otyY=
dagger.io/dagger v0.18.11/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
dagger.io/dagger v0.18.12 h1:s7v8aHlzDUogZ/jW92lHC+gljCNRML+0mosfh13R4vs=
dagger.io/dagger v0.18.12/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
github.com/99designs/gqlgen v0.17.75 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI=
github.com/99designs/gqlgen v0.17.75/go.mod h1:p7gbTpdnHyl70hmSpM8XG8GiKwmCv+T5zkdY8U8bLog=
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
@ -15,38 +27,56 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@ -61,13 +91,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -78,32 +110,34 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -113,10 +147,15 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8xLRkY=
github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
@ -125,21 +164,61 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 h1:gllJVKwONftmCc4KlNbN8o/LvmbxotqQy6zzi6yDQOQ=
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -149,31 +228,41 @@ golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

268
internal/cmd/compare.go Normal file
View File

@ -0,0 +1,268 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/manifest"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
func GetCompareCmd() *cobra.Command {
compareCmd := &cobra.Command{
Use: "compare",
Short: "Compare two feature flag manifests",
Long: "Compare two OpenFeature flag manifests and display the differences in a structured format.",
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "compare")
},
RunE: func(cmd *cobra.Command, args []string) error {
// Get flags
sourcePath := config.GetManifestPath(cmd)
targetPath, _ := cmd.Flags().GetString("against")
outputFormat, _ := cmd.Flags().GetString("output")
// Validate flags
if sourcePath == "" || targetPath == "" {
return fmt.Errorf("both source (--manifest) and target (--against) paths are required")
}
// Validate output format
if !manifest.IsValidOutputFormat(outputFormat) {
return fmt.Errorf("invalid output format: %s. Valid formats are: %s",
outputFormat, strings.Join(manifest.GetValidOutputFormats(), ", "))
}
// Load manifests
sourceManifest, err := loadManifest(sourcePath)
if err != nil {
return fmt.Errorf("error loading source manifest: %w", err)
}
targetManifest, err := loadManifest(targetPath)
if err != nil {
return fmt.Errorf("error loading target manifest: %w", err)
}
// Compare manifests
changes, err := manifest.Compare(sourceManifest, targetManifest)
if err != nil {
return fmt.Errorf("error comparing manifests: %w", err)
}
// No changes
if len(changes) == 0 {
pterm.Success.Println("No differences found between the manifests.")
return nil
}
// Render differences based on the output format
switch manifest.OutputFormat(outputFormat) {
case manifest.OutputFormatFlat:
return renderFlatDiff(changes, cmd)
case manifest.OutputFormatJSON:
return renderJSONDiff(changes, cmd)
case manifest.OutputFormatYAML:
return renderYAMLDiff(changes, cmd)
default:
return renderTreeDiff(changes, cmd)
}
},
}
// Add flags specific to compare command
compareCmd.Flags().StringP("against", "a", "", "Path to the target manifest file to compare against")
compareCmd.Flags().StringP("output", "o", string(manifest.OutputFormatTree),
fmt.Sprintf("Output format. Valid formats: %s", strings.Join(manifest.GetValidOutputFormats(), ", ")))
// Mark required flags
_ = compareCmd.MarkFlagRequired("against")
return compareCmd
}
// loadManifest loads and unmarshals a manifest file from the given path
func loadManifest(path string) (*manifest.Manifest, error) {
// Read file
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}
// Unmarshal JSON
var m manifest.Manifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("error unmarshaling JSON: %w", err)
}
return &m, nil
}
// renderTreeDiff renders changes with tree-structured inline differences
func renderTreeDiff(changes []manifest.Change, cmd *cobra.Command) error {
pterm.Info.Printf("Found %d difference(s) between manifests:\n\n", len(changes))
// Group changes by type for easier reading
var (
additions []manifest.Change
removals []manifest.Change
modifications []manifest.Change
)
for _, change := range changes {
switch change.Type {
case "add":
additions = append(additions, change)
case "remove":
removals = append(removals, change)
case "change":
modifications = append(modifications, change)
}
}
// Print additions
if len(additions) > 0 {
pterm.FgGreen.Println("◆ Additions:")
for _, change := range additions {
flagName := strings.TrimPrefix(change.Path, "flags.")
pterm.FgGreen.Printf(" + %s\n", flagName)
valueJSON, _ := json.MarshalIndent(change.NewValue, " ", " ")
fmt.Printf(" %s\n", valueJSON)
}
fmt.Println()
}
// Print removals
if len(removals) > 0 {
pterm.FgRed.Println("◆ Removals:")
for _, change := range removals {
flagName := strings.TrimPrefix(change.Path, "flags.")
pterm.FgRed.Printf(" - %s\n", flagName)
valueJSON, _ := json.MarshalIndent(change.OldValue, " ", " ")
fmt.Printf(" %s\n", valueJSON)
}
fmt.Println()
}
// Print modifications
if len(modifications) > 0 {
pterm.FgYellow.Println("◆ Modifications:")
for _, change := range modifications {
flagName := strings.TrimPrefix(change.Path, "flags.")
pterm.FgYellow.Printf(" ~ %s\n", flagName)
// Marshall the values
oldJSON, _ := json.MarshalIndent(change.OldValue, "", " ")
newJSON, _ := json.MarshalIndent(change.NewValue, "", " ")
// Print the diff
fmt.Println(" Before:")
for _, line := range strings.Split(string(oldJSON), "\n") {
fmt.Printf(" %s\n", line)
}
fmt.Println(" After:")
for _, line := range strings.Split(string(newJSON), "\n") {
fmt.Printf(" %s\n", line)
}
}
}
return nil
}
// renderFlatDiff renders changes in a flat format
func renderFlatDiff(changes []manifest.Change, cmd *cobra.Command) error {
pterm.Info.Printf("Found %d difference(s) between manifests:\n\n", len(changes))
for _, change := range changes {
flagName := strings.TrimPrefix(change.Path, "flags.")
switch change.Type {
case "add":
pterm.FgGreen.Printf("+ %s\n", flagName)
case "remove":
pterm.FgRed.Printf("- %s\n", flagName)
case "change":
pterm.FgYellow.Printf("~ %s\n", flagName)
}
}
return nil
}
// renderJSONDiff renders changes in JSON format
func renderJSONDiff(changes []manifest.Change, cmd *cobra.Command) error {
// Create a structured response that can be easily consumed by tools
type structuredOutput struct {
TotalChanges int `json:"totalChanges" yaml:"totalChanges"`
Additions []manifest.Change `json:"additions" yaml:"additions"`
Removals []manifest.Change `json:"removals" yaml:"removals"`
Modifications []manifest.Change `json:"modifications" yaml:"modifications"`
}
// Group changes by type
var output structuredOutput
output.TotalChanges = len(changes)
for _, change := range changes {
switch change.Type {
case "add":
output.Additions = append(output.Additions, change)
case "remove":
output.Removals = append(output.Removals, change)
case "change":
output.Modifications = append(output.Modifications, change)
}
}
// Convert to JSON
jsonBytes, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("error marshaling JSON output: %w", err)
}
// Print the JSON
fmt.Println(string(jsonBytes))
return nil
}
// renderYAMLDiff renders changes in YAML format
func renderYAMLDiff(changes []manifest.Change, cmd *cobra.Command) error {
// Use the same structured output type as JSON but with YAML tags
type structuredOutput struct {
TotalChanges int `json:"totalChanges" yaml:"totalChanges"`
Additions []manifest.Change `json:"additions" yaml:"additions"`
Removals []manifest.Change `json:"removals" yaml:"removals"`
Modifications []manifest.Change `json:"modifications" yaml:"modifications"`
}
// Group changes by type
var output structuredOutput
output.TotalChanges = len(changes)
for _, change := range changes {
switch change.Type {
case "add":
output.Additions = append(output.Additions, change)
case "remove":
output.Removals = append(output.Removals, change)
case "change":
output.Modifications = append(output.Modifications, change)
}
}
// Convert to YAML
yamlBytes, err := yaml.Marshal(output)
if err != nil {
return fmt.Errorf("error marshaling YAML output: %w", err)
}
// Print the YAML
fmt.Println(string(yamlBytes))
return nil
}

View File

@ -0,0 +1,50 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetCompareCmd(t *testing.T) {
cmd := GetCompareCmd()
assert.Equal(t, "compare", cmd.Use)
assert.Equal(t, "Compare two feature flag manifests", cmd.Short)
// Verify flags exist
againstFlag := cmd.Flag("against")
assert.NotNil(t, againstFlag)
// Verify output flag
outputFlag := cmd.Flag("output")
assert.NotNil(t, outputFlag)
assert.Equal(t, "tree", outputFlag.DefValue)
}
func TestCompareManifests(t *testing.T) {
// This test mainly verifies the command executes without errors
// with each of the supported output formats
formats := []string{"tree", "flat", "json", "yaml"}
for _, format := range formats {
t.Run(fmt.Sprintf("output_format_%s", format), func(t *testing.T) {
// Need to use the root command to properly inherit the manifest flag
rootCmd := GetRootCmd()
// Setup command line arguments
rootCmd.SetArgs([]string{
"compare",
"--manifest", "testdata/source_manifest.json",
"--against", "testdata/target_manifest.json",
"--output", format,
})
// Execute command
err := rootCmd.Execute()
assert.NoError(t, err, "Command should execute without errors with output format: "+format)
})
}
}

View File

@ -32,7 +32,6 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
logger.Default.Debug(fmt.Sprintf("Using config file: %s", v.ConfigFileUsed()))
}
// Track which flags were set directly via command line
cmdLineFlags := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
@ -53,7 +52,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
// Check the most specific path (e.g., generate.go.package-name)
if bindPrefix != "" {
configPaths = append(configPaths, bindPrefix + "." + f.Name)
configPaths = append(configPaths, bindPrefix+"."+f.Name)
// Check parent paths (e.g., generate.package-name)
parts := strings.Split(bindPrefix, ".")

414
internal/cmd/generate.go Normal file
View File

@ -0,0 +1,414 @@
package cmd
import (
"strings"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"github.com/open-feature/cli/internal/generators/csharp"
"github.com/open-feature/cli/internal/generators/golang"
"github.com/open-feature/cli/internal/generators/java"
"github.com/open-feature/cli/internal/generators/nestjs"
"github.com/open-feature/cli/internal/generators/nodejs"
"github.com/open-feature/cli/internal/generators/python"
"github.com/open-feature/cli/internal/generators/react"
"github.com/open-feature/cli/internal/logger"
"github.com/spf13/cobra"
)
func GetGenerateCmd() *cobra.Command {
generateCmd := &cobra.Command{
Use: "generate",
Short: "Generate typesafe OpenFeature accessors.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate")
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("Available generators:")
return generators.DefaultManager.PrintGeneratorsTable()
},
}
// Add generate flags using the config package
config.AddGenerateFlags(generateCmd)
// Add all registered generator commands
for _, subCmd := range generators.DefaultManager.GetCommands() {
generateCmd.AddCommand(subCmd)
}
addStabilityInfo(generateCmd)
return generateCmd
}
// addStabilityInfo adds stability information to the command's help template before "Usage:"
func addStabilityInfo(cmd *cobra.Command) {
// Only modify commands that have a stability annotation
if stability, ok := cmd.Annotations["stability"]; ok {
originalTemplate := cmd.UsageTemplate()
// Find the "Usage:" section and insert stability info before it
if strings.Contains(originalTemplate, "Usage:") {
customTemplate := strings.Replace(
originalTemplate,
"Usage:",
"Stability: "+stability+"\n\nUsage:",
1, // Replace only the first occurrence
)
cmd.SetUsageTemplate(customTemplate)
} else {
// Fallback if "Usage:" not found - prepend to the template
customTemplate := "Stability: " + stability + "\n\n" + originalTemplate
cmd.SetUsageTemplate(customTemplate)
}
}
}
func getGenerateNodeJSCmd() *cobra.Command {
nodeJSCmd := &cobra.Command{
Use: "nodejs",
Short: "Generate typesafe Node.js client.",
Long: `Generate typesafe Node.js client compatible with the OpenFeature JavaScript Server SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.nodejs")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Node.js")
params := generators.Params[nodejs.Params]{
OutputPath: outputPath,
Custom: nodejs.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := nodejs.NewGenerator(flagset)
logger.Default.Debug("Executing Node.js generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Node.js")
return nil
},
}
addStabilityInfo(nodeJSCmd)
return nodeJSCmd
}
func getGenerateReactCmd() *cobra.Command {
reactCmd := &cobra.Command{
Use: "react",
Short: "Generate typesafe React Hooks.",
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.react")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("React")
params := generators.Params[react.Params]{
OutputPath: outputPath,
Custom: react.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := react.NewGenerator(flagset)
logger.Default.Debug("Executing React generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("React")
return nil
},
}
addStabilityInfo(reactCmd)
return reactCmd
}
func GetGenerateNestJsCmd() *cobra.Command {
nestJsCmd := &cobra.Command{
Use: "nestjs",
Short: "Generate typesafe NestJS decorators.",
Long: `Generate typesafe NestJS decorators compatible with the OpenFeature NestJS SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.nestjs")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("NestJS")
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
nestjsParams := generators.Params[nestjs.Params]{
OutputPath: outputPath,
Custom: nestjs.Params{},
}
nestjsGenerator := nestjs.NewGenerator(flagset)
logger.Default.Debug("Executing NestJS generator")
err = nestjsGenerator.Generate(&nestjsParams)
if err != nil {
return err
}
nodejsParams := generators.Params[nodejs.Params]{
OutputPath: outputPath,
Custom: nodejs.Params{},
}
nodeGenerator := nodejs.NewGenerator(flagset)
err = nodeGenerator.Generate(&nodejsParams)
if err != nil {
return err
}
logger.Default.GenerationComplete("NestJS")
return nil
},
}
addStabilityInfo(nestJsCmd)
return nestJsCmd
}
func getGenerateCSharpCmd() *cobra.Command {
csharpCmd := &cobra.Command{
Use: "csharp",
Short: "Generate typesafe C# client.",
Long: `Generate typesafe C# client compatible with the OpenFeature .NET SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.csharp")
},
RunE: func(cmd *cobra.Command, args []string) error {
namespace := config.GetCSharpNamespace(cmd)
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("C#")
params := generators.Params[csharp.Params]{
OutputPath: outputPath,
Custom: csharp.Params{
Namespace: namespace,
},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := csharp.NewGenerator(flagset)
logger.Default.Debug("Executing C# generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("C#")
return nil
},
}
// Add C#-specific flags
config.AddCSharpGenerateFlags(csharpCmd)
addStabilityInfo(csharpCmd)
return csharpCmd
}
func getGenerateJavaCmd() *cobra.Command {
javaCmd := &cobra.Command{
Use: "java",
Short: "Generate typesafe Java client.",
Long: `Generate typesafe Java client compatible with the OpenFeature Java SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.java")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
javaPackageName := config.GetJavaPackageName(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Java")
params := generators.Params[java.Params]{
OutputPath: outputPath,
Custom: java.Params{
JavaPackage: javaPackageName,
},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := java.NewGenerator(flagset)
logger.Default.Debug("Executing Java generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Java")
return nil
},
}
// Add Java specific flags
config.AddJavaGenerateFlags(javaCmd)
addStabilityInfo(javaCmd)
return javaCmd
}
func getGenerateGoCmd() *cobra.Command {
goCmd := &cobra.Command{
Use: "go",
Short: "Generate typesafe accessors for OpenFeature.",
Long: `Generate typesafe accessors compatible with the OpenFeature Go SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.go")
},
RunE: func(cmd *cobra.Command, args []string) error {
goPackageName := config.GetGoPackageName(cmd)
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Go")
params := generators.Params[golang.Params]{
OutputPath: outputPath,
Custom: golang.Params{
GoPackage: goPackageName,
},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := golang.NewGenerator(flagset)
logger.Default.Debug("Executing Go generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Go")
return nil
},
}
// Add Go-specific flags
config.AddGoGenerateFlags(goCmd)
addStabilityInfo(goCmd)
return goCmd
}
func getGeneratePythonCmd() *cobra.Command {
pythonCmd := &cobra.Command{
Use: "python",
Short: "Generate typesafe Python client.",
Long: `Generate typesafe Python client compatible with the OpenFeature Python SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
logger.Default.GenerationStarted("Python")
params := generators.Params[python.Params]{
OutputPath: outputPath,
Custom: python.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := python.NewGenerator(flagset)
logger.Default.Debug("Executing Python generator")
err = generator.Generate(&params)
if err != nil {
return err
}
logger.Default.GenerationComplete("Python")
return nil
},
}
addStabilityInfo(pythonCmd)
return pythonCmd
}
func init() {
// Register generators with the manager
generators.DefaultManager.Register(getGenerateReactCmd)
generators.DefaultManager.Register(getGenerateGoCmd)
generators.DefaultManager.Register(getGenerateNodeJSCmd)
generators.DefaultManager.Register(getGeneratePythonCmd)
generators.DefaultManager.Register(getGenerateCSharpCmd)
generators.DefaultManager.Register(GetGenerateNestJsCmd)
generators.DefaultManager.Register(getGenerateJavaCmd)
}

View File

@ -21,7 +21,7 @@ type generateTestCase struct {
outputGolden string // path to the golden output file
outputPath string // output directory (optional, defaults to "output")
outputFile string // output file name
packageName string // optional, only used for Go
packageName string // optional, used for Go (package-name), Java (package-name) and C# (namespace)
}
func TestGenerate(t *testing.T) {
@ -48,6 +48,36 @@ func TestGenerate(t *testing.T) {
outputGolden: "testdata/success_nodejs.golden",
outputFile: "openfeature.ts",
},
{
name: "NestJS generation success",
command: "nestjs",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_nestjs.golden",
outputFile: "openfeature-decorators.ts",
},
{
name: "Python generation success",
command: "python",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_python.golden",
outputFile: "openfeature.py",
},
{
name: "CSharp generation success",
command: "csharp",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_csharp.golden",
outputFile: "OpenFeature.g.cs",
packageName: "TestNamespace", // Using packageName field for namespace
},
{
name: "Java generation success",
command: "java",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_java.golden",
outputFile: "OpenFeature.java",
packageName: "com.example.openfeature",
},
// Add more test cases here as needed
}
@ -79,9 +109,15 @@ func TestGenerate(t *testing.T) {
"--output", outputPath,
}
// Add package name if provided (for Go)
// Add parameters specific to each generator
if tc.packageName != "" {
args = append(args, "--package-name", tc.packageName)
if tc.command == "csharp" {
args = append(args, "--namespace", tc.packageName)
} else if tc.command == "go" {
args = append(args, "--package-name", tc.packageName)
} else if tc.command == "java" {
args = append(args, "--package-name", tc.packageName)
}
}
cmd.SetArgs(args)
@ -100,7 +136,7 @@ func TestGenerate(t *testing.T) {
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
data, err := os.ReadFile(inputPath)
if (err != nil) {
if err != nil {
t.Fatalf("error reading file %q: %v", inputPath, err)
}
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
@ -120,6 +156,17 @@ func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string,
}
}
// normalizeLines trims trailing whitespace and carriage returns from each line.
// This helps ensure consistent comparison by ignoring formatting differences like indentation or line endings.
func normalizeLines(input []string) []string {
normalized := make([]string, len(input))
for i, line := range input {
// Trim right whitespace and convert \r\n or \r to \n
normalized[i] = strings.TrimRight(line, " \t\r")
}
return normalized
}
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
want, err := os.ReadFile(testFile)
if err != nil {
@ -132,8 +179,8 @@ func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs)
}
// Convert to string arrays by splitting on newlines
wantLines := strings.Split(string(want), "\n")
gotLines := strings.Split(string(got), "\n")
wantLines := normalizeLines(strings.Split(string(want), "\n"))
gotLines := normalizeLines(strings.Split(string(got), "\n"))
if diff := cmp.Diff(wantLines, gotLines); diff != "" {
t.Errorf("output mismatch (-want +got):\n%s", diff)

28
internal/cmd/init_test.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
import (
"testing"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/filesystem"
"github.com/spf13/afero"
)
func TestInitCmd(t *testing.T) {
fs := afero.NewMemMapFs()
filesystem.SetFileSystem(fs)
outputFile := "flags-test.json"
cmd := GetInitCmd()
// global flag exists on root only.
config.AddRootFlags(cmd)
cmd.SetArgs([]string{
"-m",
outputFile,
})
err := cmd.Execute()
if err != nil {
t.Error(err)
}
compareOutput(t, "testdata/success_init.golden", outputFile, fs)
}

View File

@ -44,7 +44,7 @@ func GetRootCmd() *cobra.Command {
},
RunE: func(cmd *cobra.Command, args []string) error {
printBanner()
logger.Default.Println("");
logger.Default.Println("")
logger.Default.Println("To see all the options, try 'openfeature --help'")
return nil
},
@ -62,6 +62,7 @@ func GetRootCmd() *cobra.Command {
rootCmd.AddCommand(GetVersionCmd())
rootCmd.AddCommand(GetInitCmd())
rootCmd.AddCommand(GetGenerateCmd())
rootCmd.AddCommand(GetCompareCmd())
// Add a custom error handler after the command is created
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {

View File

@ -0,0 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
"flags": {
"darkMode": {
"flagType": "boolean",
"description": "Enable dark mode",
"defaultValue": false
},
"backgroundColor": {
"flagType": "string",
"description": "Background color for the application",
"defaultValue": "white"
},
"maxItems": {
"flagType": "integer",
"description": "Maximum number of items to display",
"defaultValue": 10
}
}
}

View File

@ -0,0 +1,251 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using OpenFeature;
using OpenFeature.Model;
namespace TestNamespace
{
/// <summary>
/// Service collection extensions for OpenFeature
/// </summary>
public static class OpenFeatureServiceExtensions
{
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient())
.AddSingleton<GeneratedClient>();
}
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client for a specific domain
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <param name="domain">The domain to get the client for</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient(domain))
.AddSingleton<GeneratedClient>();
}
}
/// <summary>
/// Generated OpenFeature client for typesafe flag access
/// </summary>
public class GeneratedClient
{
private readonly IFeatureClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
/// </summary>
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
public GeneratedClient(IFeatureClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<double> DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<double>> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<bool> EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<string> GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<int> UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Creates a new GeneratedClient using the default OpenFeature client
/// </summary>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient()
{
return new GeneratedClient(Api.Instance.GetClient());
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <param name="evaluationContext">Default context to use for evaluations</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
}
}

View File

@ -14,6 +14,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext
type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error)
type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error)
type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error)
type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error)
var client openfeature.IClient = nil
// Discount percentage applied to purchases.
@ -67,6 +69,23 @@ var GreetingMessage = struct {
return client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx)
},
}
// Allows customization of theme colors.
var ThemeCustomization = struct {
// Value returns the value of the flag ThemeCustomization,
// as well as the evaluation error, if present.
Value ObjectProvider
// ValueWithDetails returns the value of the flag ThemeCustomization,
// the evaluation error, if any, and the evaluation details.
ValueWithDetails ObjectProviderDetails
}{
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error) {
return client.ObjectValue(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error){
return client.ObjectValueDetails(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
},
}
// Maximum allowed length for usernames.
var UsernameMaxLength = struct {
// Value returns the value of the flag UsernameMaxLength,

View File

@ -0,0 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/open-feature/cli/main/schema/v0/flag-manifest.json",
"flags": {}
}

View File

@ -0,0 +1,184 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
package com.example.openfeature;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.OpenFeatureAPI;
public final class OpenFeature {
private OpenFeature() {} // prevent instantiation
public interface GeneratedClient {
/**
* Discount percentage applied to purchases.
* Details:
* - Flag key: discountPercentage
* - Type: Double
* - Default value: 0.15
* Returns the flag value
*/
Double discountPercentage(EvaluationContext ctx);
/**
* Discount percentage applied to purchases.
* Details:
* - Flag key: discountPercentage
* - Type: Double
* - Default value: 0.15
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<Double> discountPercentageDetails(EvaluationContext ctx);
/**
* Controls whether Feature A is enabled.
* Details:
* - Flag key: enableFeatureA
* - Type: Boolean
* - Default value: false
* Returns the flag value
*/
Boolean enableFeatureA(EvaluationContext ctx);
/**
* Controls whether Feature A is enabled.
* Details:
* - Flag key: enableFeatureA
* - Type: Boolean
* - Default value: false
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<Boolean> enableFeatureADetails(EvaluationContext ctx);
/**
* The message to use for greeting users.
* Details:
* - Flag key: greetingMessage
* - Type: String
* - Default value: Hello there!
* Returns the flag value
*/
String greetingMessage(EvaluationContext ctx);
/**
* The message to use for greeting users.
* Details:
* - Flag key: greetingMessage
* - Type: String
* - Default value: Hello there!
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<String> greetingMessageDetails(EvaluationContext ctx);
/**
* Allows customization of theme colors.
* Details:
* - Flag key: themeCustomization
* - Type: Object
* - Default value: Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d")
* Returns the flag value
*/
Object themeCustomization(EvaluationContext ctx);
/**
* Allows customization of theme colors.
* Details:
* - Flag key: themeCustomization
* - Type: Object
* - Default value: Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d")
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<Object> themeCustomizationDetails(EvaluationContext ctx);
/**
* Maximum allowed length for usernames.
* Details:
* - Flag key: usernameMaxLength
* - Type: Integer
* - Default value: 50
* Returns the flag value
*/
Integer usernameMaxLength(EvaluationContext ctx);
/**
* Maximum allowed length for usernames.
* Details:
* - Flag key: usernameMaxLength
* - Type: Integer
* - Default value: 50
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<Integer> usernameMaxLengthDetails(EvaluationContext ctx);
}
private static final class OpenFeatureGeneratedClient implements GeneratedClient {
private final Client client;
private OpenFeatureGeneratedClient(Client client) {
this.client = client;
}
@Override
public Double discountPercentage(EvaluationContext ctx) {
return client.getDoubleValue("discountPercentage", 0.15, ctx);
}
@Override
public FlagEvaluationDetails<Double> discountPercentageDetails(EvaluationContext ctx) {
return client.getDoubleDetails("discountPercentage", 0.15, ctx);
}
@Override
public Boolean enableFeatureA(EvaluationContext ctx) {
return client.getBooleanValue("enableFeatureA", false, ctx);
}
@Override
public FlagEvaluationDetails<Boolean> enableFeatureADetails(EvaluationContext ctx) {
return client.getBooleanDetails("enableFeatureA", false, ctx);
}
@Override
public String greetingMessage(EvaluationContext ctx) {
return client.getStringValue("greetingMessage", "Hello there!", ctx);
}
@Override
public FlagEvaluationDetails<String> greetingMessageDetails(EvaluationContext ctx) {
return client.getStringDetails("greetingMessage", "Hello there!", ctx);
}
@Override
public Object themeCustomization(EvaluationContext ctx) {
return client.getObjectValue("themeCustomization", Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d"), ctx);
}
@Override
public FlagEvaluationDetails<Object> themeCustomizationDetails(EvaluationContext ctx) {
return client.getObjectDetails("themeCustomization", Map.of("primaryColor", "#007bff", "secondaryColor", "#6c757d"), ctx);
}
@Override
public Integer usernameMaxLength(EvaluationContext ctx) {
return client.getIntegerValue("usernameMaxLength", 50, ctx);
}
@Override
public FlagEvaluationDetails<Integer> usernameMaxLengthDetails(EvaluationContext ctx) {
return client.getIntegerDetails("usernameMaxLength", 50, ctx);
}
}
public static GeneratedClient getClient() {
return new OpenFeatureGeneratedClient(OpenFeatureAPI.getInstance().getClient());
}
public static GeneratedClient getClient(String domain) {
return new OpenFeatureGeneratedClient(OpenFeatureAPI.getInstance().getClient(domain));
}
}

View File

@ -0,0 +1,220 @@
import type { DynamicModule, FactoryProvider as NestFactoryProvider } from "@nestjs/common";
import { Inject, Module } from "@nestjs/common";
import type { Observable } from "rxjs";
import type {
OpenFeature,
Client,
EvaluationContext,
EvaluationDetails,
OpenFeatureModuleOptions,
JsonValue
} from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk";
import type { GeneratedClient } from "./openfeature";
import { getGeneratedClient } from "./openfeature";
/**
* Returns an injection token for a (domain scoped) generated OpenFeature client.
* @param {string} domain The domain of the generated OpenFeature client.
* @returns {string} The injection token.
*/
export function getOpenFeatureGeneratedClientToken(domain?: string): string {
return domain ? `OpenFeatureGeneratedClient_${domain}` : "OpenFeatureGeneratedClient_default";
}
/**
* Options for injecting an OpenFeature client into a constructor.
*/
interface FeatureClientProps {
/**
* The domain of the OpenFeature client, if a domain scoped client should be used.
* @see {@link Client.getBooleanDetails}
*/
domain?: string;
}
/**
* Injects a generated typesafe feature client into a constructor or property of a class.
* @param {FeatureClientProps} [props] The options for injecting the client.
* @returns {PropertyDecorator & ParameterDecorator} The decorator function.
*/
export const GeneratedOpenFeatureClient = (props?: FeatureClientProps): PropertyDecorator & ParameterDecorator =>
Inject(getOpenFeatureGeneratedClientToken(props?.domain));
/**
* GeneratedOpenFeatureModule is a generated typesafe NestJS wrapper for OpenFeature Server-SDK.
*/
@Module({})
export class GeneratedOpenFeatureModule extends OpenFeatureModule {
static override forRoot({ useGlobalInterceptor = true, ...options }: OpenFeatureModuleOptions): DynamicModule {
const module = super.forRoot({ useGlobalInterceptor, ...options });
const clientValueProviders: NestFactoryProvider<GeneratedClient>[] = [
{
provide: getOpenFeatureGeneratedClientToken(),
useFactory: () => getGeneratedClient(),
},
];
if (options?.providers) {
const domainClientProviders: NestFactoryProvider<GeneratedClient>[] = Object.keys(options.providers).map(
(domain) => ({
provide: getOpenFeatureGeneratedClientToken(domain),
useFactory: () => getGeneratedClient(domain),
}),
);
clientValueProviders.push(...domainClientProviders);
}
return {
...module,
providers: module.providers ? [...module.providers, ...clientValueProviders] : clientValueProviders,
exports: module.exports ? [...module.exports, ...clientValueProviders] : clientValueProviders,
};
}
}
/**
* Options for injecting a typed feature flag into a route handler.
*/
interface TypedFeatureProps {
/**
* The domain of the OpenFeature client, if a domain scoped client should be used.
* @see {@link OpenFeature#getClient}
*/
domain?: string;
/**
* The {@link EvaluationContext} for evaluating the feature flag.
* @see {@link OpenFeature#getClient}
*/
context?: EvaluationContext;
}
/**
* Gets the {@link EvaluationDetails} for `discountPercentage` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `discountPercentage`
* - description: `Discount percentage applied to purchases.`
* - default value: `0.15`
* - type: `number`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @DiscountPercentage()
* discountPercentage: Observable<EvaluationDetails<number>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function DiscountPercentage(props?: TypedFeatureProps): ParameterDecorator {
return NumberFeatureFlag({ flagKey: "discountPercentage", defaultValue: 0.15, ...props });
}
/**
* Gets the {@link EvaluationDetails} for `enableFeatureA` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `enableFeatureA`
* - description: `Controls whether Feature A is enabled.`
* - default value: `false`
* - type: `boolean`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @EnableFeatureA()
* enableFeatureA: Observable<EvaluationDetails<boolean>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function EnableFeatureA(props?: TypedFeatureProps): ParameterDecorator {
return BooleanFeatureFlag({ flagKey: "enableFeatureA", defaultValue: false, ...props });
}
/**
* Gets the {@link EvaluationDetails} for `greetingMessage` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `greetingMessage`
* - description: `The message to use for greeting users.`
* - default value: `Hello there!`
* - type: `string`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @GreetingMessage()
* greetingMessage: Observable<EvaluationDetails<string>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function GreetingMessage(props?: TypedFeatureProps): ParameterDecorator {
return StringFeatureFlag({ flagKey: "greetingMessage", defaultValue: "Hello there!", ...props });
}
/**
* Gets the {@link EvaluationDetails} for `themeCustomization` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `themeCustomization`
* - description: `Allows customization of theme colors.`
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
* - type: `JsonValue`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @ThemeCustomization()
* themeCustomization: Observable<EvaluationDetails<JsonValue>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function ThemeCustomization(props?: TypedFeatureProps): ParameterDecorator {
return ObjectFeatureFlag({ flagKey: "themeCustomization", defaultValue: {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, ...props });
}
/**
* Gets the {@link EvaluationDetails} for `usernameMaxLength` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `usernameMaxLength`
* - description: `Maximum allowed length for usernames.`
* - default value: `50`
* - type: `number`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @UsernameMaxLength()
* usernameMaxLength: Observable<EvaluationDetails<number>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function UsernameMaxLength(props?: TypedFeatureProps): ParameterDecorator {
return NumberFeatureFlag({ flagKey: "usernameMaxLength", defaultValue: 50, ...props });
}

View File

@ -3,6 +3,7 @@ import {
OpenFeature,
stringOrUndefined,
objectOrUndefined,
JsonValue,
} from "@openfeature/server-sdk";
import type {
EvaluationContext,
@ -101,6 +102,36 @@ export interface GeneratedClient {
*/
greetingMessageDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<string>>;
/**
* Allows customization of theme colors.
*
* **Details:**
* - flag key: `themeCustomization`
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
* - type: `JsonValue`
*
* Performs a flag evaluation that returns a object.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<JsonValue>} Flag evaluation response
*/
themeCustomization(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<JsonValue>;
/**
* Allows customization of theme colors.
*
* **Details:**
* - flag key: `themeCustomization`
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
* - type: `JsonValue`
*
* Performs a flag evaluation that a returns an evaluation details object.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<JsonValue>>} Flag evaluation details response
*/
themeCustomizationDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<JsonValue>>;
/**
* Maximum allowed length for usernames.
*
@ -185,6 +216,14 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
return client.getStringDetails("greetingMessage", "Hello there!", context, options);
},
themeCustomization: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<JsonValue> => {
return client.getObjectValue("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options);
},
themeCustomizationDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<JsonValue>> => {
return client.getObjectDetails("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options);
},
usernameMaxLength: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number> => {
return client.getNumberValue("usernameMaxLength", 50, context, options);
},

View File

@ -0,0 +1,472 @@
# AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
from typing import Optional
from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagEvaluationOptions
from openfeature.hook import Hook
class GeneratedClient:
def __init__(
self,
client: OpenFeatureClient,
) -> None:
self.client = client
def discount_percentage(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> float:
"""
Discount percentage applied to purchases.
**Details:**
- flag key: `discountPercentage`
- default value: `0.15`
- type: `float`
Performs a flag evaluation that returns a `float`.
"""
return self.client.get_float_value(
flag_key="discountPercentage",
default_value=0.15,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def discount_percentage_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Discount percentage applied to purchases.
**Details:**
- flag key: `discountPercentage`
- default value: `0.15`
- type: `float`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.get_float_details(
flag_key="discountPercentage",
default_value=0.15,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def discount_percentage_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> float:
"""
Discount percentage applied to purchases.
**Details:**
- flag key: `discountPercentage`
- default value: `0.15`
- type: `float`
Performs a flag evaluation asynchronously and returns a `float`.
"""
return await self.client.get_float_value_async(
flag_key="discountPercentage",
default_value=0.15,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def discount_percentage_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Discount percentage applied to purchases.
**Details:**
- flag key: `discountPercentage`
- default value: `0.15`
- type: `float`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.get_float_details_async(
flag_key="discountPercentage",
default_value=0.15,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def enable_feature_a(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> bool:
"""
Controls whether Feature A is enabled.
**Details:**
- flag key: `enableFeatureA`
- default value: `False`
- type: `bool`
Performs a flag evaluation that returns a `bool`.
"""
return self.client.get_boolean_value(
flag_key="enableFeatureA",
default_value=False,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def enable_feature_a_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Controls whether Feature A is enabled.
**Details:**
- flag key: `enableFeatureA`
- default value: `False`
- type: `bool`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.get_boolean_details(
flag_key="enableFeatureA",
default_value=False,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def enable_feature_a_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> bool:
"""
Controls whether Feature A is enabled.
**Details:**
- flag key: `enableFeatureA`
- default value: `False`
- type: `bool`
Performs a flag evaluation asynchronously and returns a `bool`.
"""
return await self.client.get_boolean_value_async(
flag_key="enableFeatureA",
default_value=False,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def enable_feature_a_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Controls whether Feature A is enabled.
**Details:**
- flag key: `enableFeatureA`
- default value: `False`
- type: `bool`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.get_boolean_details_async(
flag_key="enableFeatureA",
default_value=False,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def greeting_message(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> str:
"""
The message to use for greeting users.
**Details:**
- flag key: `greetingMessage`
- default value: `Hello there!`
- type: `str`
Performs a flag evaluation that returns a `str`.
"""
return self.client.get_string_value(
flag_key="greetingMessage",
default_value="Hello there!",
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def greeting_message_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
The message to use for greeting users.
**Details:**
- flag key: `greetingMessage`
- default value: `Hello there!`
- type: `str`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.get_string_details(
flag_key="greetingMessage",
default_value="Hello there!",
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def greeting_message_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> str:
"""
The message to use for greeting users.
**Details:**
- flag key: `greetingMessage`
- default value: `Hello there!`
- type: `str`
Performs a flag evaluation asynchronously and returns a `str`.
"""
return await self.client.get_string_value_async(
flag_key="greetingMessage",
default_value="Hello there!",
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def greeting_message_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
The message to use for greeting users.
**Details:**
- flag key: `greetingMessage`
- default value: `Hello there!`
- type: `str`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.get_string_details_async(
flag_key="greetingMessage",
default_value="Hello there!",
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def theme_customization(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> object:
"""
Allows customization of theme colors.
**Details:**
- flag key: `themeCustomization`
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
- type: `object`
Performs a flag evaluation that returns a `object`.
"""
return self.client.get_object_value(
flag_key="themeCustomization",
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def theme_customization_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Allows customization of theme colors.
**Details:**
- flag key: `themeCustomization`
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
- type: `object`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.get_object_details(
flag_key="themeCustomization",
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def theme_customization_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> object:
"""
Allows customization of theme colors.
**Details:**
- flag key: `themeCustomization`
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
- type: `object`
Performs a flag evaluation asynchronously and returns a `object`.
"""
return await self.client.get_object_value_async(
flag_key="themeCustomization",
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def theme_customization_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Allows customization of theme colors.
**Details:**
- flag key: `themeCustomization`
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
- type: `object`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.get_object_details_async(
flag_key="themeCustomization",
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def username_max_length(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> int:
"""
Maximum allowed length for usernames.
**Details:**
- flag key: `usernameMaxLength`
- default value: `50`
- type: `int`
Performs a flag evaluation that returns a `int`.
"""
return self.client.get_integer_value(
flag_key="usernameMaxLength",
default_value=50,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def username_max_length_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Maximum allowed length for usernames.
**Details:**
- flag key: `usernameMaxLength`
- default value: `50`
- type: `int`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.get_integer_details(
flag_key="usernameMaxLength",
default_value=50,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def username_max_length_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> int:
"""
Maximum allowed length for usernames.
**Details:**
- flag key: `usernameMaxLength`
- default value: `50`
- type: `int`
Performs a flag evaluation asynchronously and returns a `int`.
"""
return await self.client.get_integer_value_async(
flag_key="usernameMaxLength",
default_value=50,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def username_max_length_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
Maximum allowed length for usernames.
**Details:**
- flag key: `usernameMaxLength`
- default value: `50`
- type: `int`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.get_integer_details_async(
flag_key="usernameMaxLength",
default_value=50,
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def get_generated_client(
client: Optional[OpenFeatureClient] = None,
domain: Optional[str] = None,
version: Optional[str] = None,
context: Optional[EvaluationContext] = None,
hooks: Optional[list[Hook]] = None,
) -> GeneratedClient:
if not client:
client = OpenFeatureClient(
domain=domain,
version=version,
context=context,
hooks=hooks,
)
return GeneratedClient(client)

View File

@ -5,6 +5,7 @@ import {
type ReactFlagEvaluationNoSuspenseOptions,
useFlag,
useSuspenseFlag,
JsonValue
} from "@openfeature/react-sdk";
/**
@ -88,6 +89,33 @@ export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspen
return useSuspenseFlag("greetingMessage", "Hello there!", options);
};
/**
* Allows customization of theme colors.
*
* **Details:**
* - flag key: `themeCustomization`
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
* - type: `JsonValue`
*/
export const useThemeCustomization = (options?: ReactFlagEvaluationOptions) => {
return useFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options);
};
/**
* Allows customization of theme colors.
*
* **Details:**
* - flag key: `themeCustomization`
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
* - type: `JsonValue`
*
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseThemeCustomization = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options);
};
/**
* Maximum allowed length for usernames.
*

View File

@ -0,0 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
"flags": {
"darkMode": {
"flagType": "boolean",
"description": "Enable dark mode for the application",
"defaultValue": true
},
"backgroundColor": {
"flagType": "string",
"description": "Background color for the application",
"defaultValue": "black"
},
"welcomeMessage": {
"flagType": "string",
"description": "Welcome message to display",
"defaultValue": "Hello, Welcome to OpenFeature!"
}
}
}

View File

@ -6,19 +6,23 @@ import (
// Flag name constants to avoid duplication
const (
DebugFlagName = "debug"
ManifestFlagName = "manifest"
OutputFlagName = "output"
NoInputFlagName = "no-input"
GoPackageFlagName = "package-name"
OverrideFlagName = "override"
DebugFlagName = "debug"
ManifestFlagName = "manifest"
OutputFlagName = "output"
NoInputFlagName = "no-input"
GoPackageFlagName = "package-name"
CSharpNamespaceName = "namespace"
OverrideFlagName = "override"
JavaPackageFlagName = "package-name"
)
// Default values for flags
const (
DefaultManifestPath = "flags.json"
DefaultOutputPath = ""
DefaultGoPackageName = "openfeature"
DefaultManifestPath = "flags.json"
DefaultOutputPath = ""
DefaultGoPackageName = "openfeature"
DefaultCSharpNamespace = "OpenFeature"
DefaultJavaPackageName = "com.example.openfeature"
)
// AddRootFlags adds the common flags to the given command
@ -38,6 +42,16 @@ func AddGoGenerateFlags(cmd *cobra.Command) {
cmd.Flags().String(GoPackageFlagName, DefaultGoPackageName, "Name of the generated Go package")
}
// AddCSharpGenerateFlags adds the C# generator specific flags to the given command
func AddCSharpGenerateFlags(cmd *cobra.Command) {
cmd.Flags().String(CSharpNamespaceName, DefaultCSharpNamespace, "Namespace for the generated C# code")
}
// AddJavaGenerateFlags adds the Java generator specific flags to the given command
func AddJavaGenerateFlags(cmd *cobra.Command) {
cmd.Flags().String(JavaPackageFlagName, DefaultJavaPackageName, "Name of the generated Java package")
}
// AddInitFlags adds the init command specific flags
func AddInitFlags(cmd *cobra.Command) {
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
@ -61,6 +75,18 @@ func GetGoPackageName(cmd *cobra.Command) string {
return goPackageName
}
// GetCSharpNamespace gets the C# namespace from the given command
func GetCSharpNamespace(cmd *cobra.Command) string {
namespace, _ := cmd.Flags().GetString(CSharpNamespaceName)
return namespace
}
// GetJavaPackageName gets the Java package name from the given command
func GetJavaPackageName(cmd *cobra.Command) string {
javaPackageName, _ := cmd.Flags().GetString(JavaPackageFlagName)
return javaPackageName
}
// GetNoInput gets the no-input flag from the given command
func GetNoInput(cmd *cobra.Command) bool {
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"sort"
"strings"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/manifest"
@ -27,24 +28,24 @@ const (
func (f FlagType) String() string {
switch f {
case IntType:
return "int"
return "int"
case FloatType:
return "float"
return "float"
case BoolType:
return "bool"
return "bool"
case StringType:
return "string"
return "string"
case ObjectType:
return "object"
return "object"
default:
return "unknown"
return "unknown"
}
}
type Flag struct {
Key string
Type FlagType
Description string
Key string
Type FlagType
Description string
DefaultValue any
}
@ -64,7 +65,7 @@ func Load(manifestPath string) (*Flagset, error) {
if err != nil {
return nil, err
} else if len(validationErrors) > 0 {
return nil, fmt.Errorf("validation failed: %v", validationErrors)
return nil, errors.New(FormatValidationError(validationErrors))
}
var flagset Flagset
@ -90,9 +91,9 @@ func (fs *Flagset) Filter(unsupportedFlagTypes map[FlagType]bool) *Flagset {
func (fs *Flagset) UnmarshalJSON(data []byte) error {
var manifest struct {
Flags map[string]struct {
FlagType string `json:"flagType"`
Description string `json:"description"`
DefaultValue any `json:"defaultValue"`
FlagType string `json:"flagType"`
Description string `json:"description"`
DefaultValue any `json:"defaultValue"`
} `json:"flags"`
}
@ -132,3 +133,43 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
return nil
}
func FormatValidationError(issues []manifest.ValidationError) string {
var sb strings.Builder
sb.WriteString("flag manifest validation failed:\n\n")
// Group messages by flag path
grouped := make(map[string]struct {
flagType string
messages []string
})
for _, issue := range issues {
entry := grouped[issue.Path]
entry.flagType = issue.Type
entry.messages = append(entry.messages, issue.Message)
grouped[issue.Path] = entry
}
// Sort paths for consistent output
paths := make([]string, 0, len(grouped))
for path := range grouped {
paths = append(paths, path)
}
sort.Strings(paths)
// Format each row
for _, path := range paths {
entry := grouped[path]
flagType := entry.flagType
if flagType == "" {
flagType = "missing"
}
sb.WriteString(fmt.Sprintf(
"- flagType: %s\n flagPath: %s\n errors:\n ~ %s\n \tSuggestions:\n \t- flagType: boolean\n \t- defaultValue: true\n\n",
flagType,
path,
strings.Join(entry.messages, "\n ~ "),
))
}
return sb.String()
}

View File

@ -0,0 +1,29 @@
package flagset
import (
"strings"
"testing"
"github.com/open-feature/cli/internal/manifest"
)
// Sample test for FormatValidationError
func TestFormatValidationError_SortsByPath(t *testing.T) {
issues := []manifest.ValidationError{
{Path: "zeta.flag", Type: "boolean", Message: "must not be empty"},
{Path: "alpha.flag", Type: "string", Message: "invalid value"},
{Path: "beta.flag", Type: "number", Message: "must be greater than zero"},
}
output := FormatValidationError(issues)
// The output should mention 'alpha.flag' before 'beta.flag', and 'beta.flag' before 'zeta.flag'
alphaIdx := strings.Index(output, "flagPath: alpha.flag")
betaIdx := strings.Index(output, "flagPath: beta.flag")
zetaIdx := strings.Index(output, "flagPath: zeta.flag")
if !(alphaIdx < betaIdx && betaIdx < zetaIdx) {
t.Errorf("flag paths are not sorted: alphaIdx=%d, betaIdx=%d, zetaIdx=%d\nOutput:\n%s",
alphaIdx, betaIdx, zetaIdx, output)
}
}

View File

@ -0,0 +1,144 @@
package csharp
import (
_ "embed"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type CsharpGenerator struct {
generators.CommonGenerator
}
type Params struct {
// Add C# specific parameters here if needed
Namespace string
}
//go:embed csharp.tmpl
var csharpTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
return "int"
case flagset.FloatType:
return "double" // .NET uses double, not float
case flagset.BoolType:
return "bool"
case flagset.StringType:
return "string"
case flagset.ObjectType:
return "object"
default:
return ""
}
}
func formatDefaultValue(flag flagset.Flag) string {
switch flag.Type {
case flagset.StringType:
return fmt.Sprintf("\"%s\"", flag.DefaultValue)
case flagset.BoolType:
if flag.DefaultValue == true {
return "true"
}
return "false"
default:
return fmt.Sprintf("%v", flag.DefaultValue)
}
}
func toCSharpDict(value any) string {
assertedMap, ok := value.(map[string]any)
if !ok {
return "null"
}
keys := slices.Sorted(maps.Keys(assertedMap))
var builder strings.Builder
builder.WriteString("new Value(Structure.Builder()")
for _, key := range keys {
val := assertedMap[key]
builder.WriteString(fmt.Sprintf(".Set(%q, %s)", key, formatNestedValue(val)))
}
builder.WriteString(".Build())")
return builder.String()
}
func formatNestedValue(value any) string {
switch val := value.(type) {
case string:
flag := flagset.Flag{
Type: flagset.StringType,
DefaultValue: val,
}
return formatDefaultValue(flag)
case bool:
flag := flagset.Flag{
Type: flagset.BoolType,
DefaultValue: val,
}
return formatDefaultValue(flag)
case int, int64:
flag := flagset.Flag{
Type: flagset.IntType,
DefaultValue: val,
}
return formatDefaultValue(flag)
case float64:
flag := flagset.Flag{
Type: flagset.FloatType,
DefaultValue: val,
}
return formatDefaultValue(flag)
case map[string]any:
return toCSharpDict(val)
case []any:
var sliceBuilder strings.Builder
sliceBuilder.WriteString("new Value(new List<Value>{")
for index, elem := range val {
if index > 0 {
sliceBuilder.WriteString(", ")
}
sliceBuilder.WriteString(formatNestedValue(elem))
}
sliceBuilder.WriteString("})")
return sliceBuilder.String()
default:
return fmt.Sprintf("new Value(%s)", val)
}
}
func (g *CsharpGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"FormatDefaultValue": formatDefaultValue,
"ToCSharpDict": toCSharpDict,
}
newParams := &generators.Params[any]{
OutputPath: params.OutputPath,
Custom: params.Custom,
}
return g.GenerateFile(funcs, csharpTmpl, newParams, "OpenFeature.g.cs")
}
// NewGenerator creates a generator for C#.
func NewGenerator(fs *flagset.Flagset) *CsharpGenerator {
return &CsharpGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,149 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using OpenFeature;
using OpenFeature.Model;
namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else }}OpenFeatureGenerated{{ end }}
{
/// <summary>
/// Service collection extensions for OpenFeature
/// </summary>
public static class OpenFeatureServiceExtensions
{
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient())
.AddSingleton<GeneratedClient>();
}
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client for a specific domain
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <param name="domain">The domain to get the client for</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient(domain))
.AddSingleton<GeneratedClient>();
}
}
/// <summary>
/// Generated OpenFeature client for typesafe flag access
/// </summary>
public class GeneratedClient
{
private readonly IFeatureClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
/// </summary>
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
public GeneratedClient(IFeatureClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
{{- range .Flagset.Flags }}
/// <summary>
/// {{ .Description }}
/// </summary>
/// <remarks>
/// <para>Flag key: {{ .Key }}</para>
/// <para>Default value: {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ .DefaultValue }}{{ end }}</para>
/// <para>Type: {{ .Type | OpenFeatureType }}</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<{{ if eq (.Type | OpenFeatureType) "object" }}Value{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> {{ .Key | ToPascal }}Async(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
{{- if eq .Type 1 }}
return await _client.GetIntegerValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 2 }}
return await _client.GetDoubleValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 3 }}
return await _client.GetBooleanValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 4 }}
return await _client.GetStringValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 5 }}
return await _client.GetObjectValueAsync("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, evaluationContext, options);
{{- else }}
throw new NotSupportedException("Unsupported flag type");
{{- end }}
}
/// <summary>
/// {{ .Description }}
/// </summary>
/// <remarks>
/// <para>Flag key: {{ .Key }}</para>
/// <para>Default value: {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ .DefaultValue }}{{ end }}</para>
/// <para>Type: {{ .Type | OpenFeatureType }}</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}Value{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>> {{ .Key | ToPascal }}DetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
{{- if eq .Type 1 }}
return await _client.GetIntegerDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 2 }}
return await _client.GetDoubleDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 3 }}
return await _client.GetBooleanDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 4 }}
return await _client.GetStringDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
{{- else if eq .Type 5 }}
return await _client.GetObjectDetailsAsync("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "object" }}{{ .DefaultValue | ToCSharpDict }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, evaluationContext, options);
{{- else }}
throw new NotSupportedException("Unsupported flag type");
{{- end }}
}
{{ end }}
/// <summary>
/// Creates a new GeneratedClient using the default OpenFeature client
/// </summary>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient()
{
return new GeneratedClient(Api.Instance.GetClient());
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <param name="evaluationContext">Default context to use for evaluations</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
}
}

View File

@ -17,16 +17,16 @@ func defaultFuncs() template.FuncMap {
"ToPascal": strcase.ToCamel,
// Remapping ToLowerCamel to ToCamel to match the expected behavior
// Ref: See above
"ToCamel": strcase.ToLowerCamel,
"ToKebab": strcase.ToKebab,
"ToCamel": strcase.ToLowerCamel,
"ToKebab": strcase.ToKebab,
"ToScreamingKebab": strcase.ToScreamingKebab,
"ToSnake": strcase.ToSnake,
"ToSnake": strcase.ToSnake,
"ToScreamingSnake": strcase.ToScreamingSnake,
"ToUpper": strings.ToUpper,
"ToLower": strings.ToLower,
"Title": cases.Title,
"Quote": strconv.Quote,
"QuoteString": func (input any) any {
"ToUpper": strings.ToUpper,
"ToLower": strings.ToLower,
"Title": cases.Title,
"Quote": strconv.Quote,
"QuoteString": func(input any) any {
if str, ok := input.(string); ok {
return strconv.Quote(str)
}

View File

@ -2,7 +2,12 @@ package golang
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"sort"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -30,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string {
return "Boolean"
case flagset.StringType:
return "String"
case flagset.ObjectType:
return "Object"
default:
return ""
}
@ -45,6 +52,8 @@ func typeString(flagType flagset.FlagType) string {
return "bool"
case flagset.FloatType:
return "float64"
case flagset.ObjectType:
return "map[string]any"
default:
return ""
}
@ -60,11 +69,68 @@ func supportImports(flags []flagset.Flag) []string {
return res
}
func toMapLiteral(value any) string {
assertedMap, ok := value.(map[string]any)
if !ok {
return "nil"
}
// To have a determined order of the object for comparison
keys := slices.Sorted(maps.Keys(assertedMap))
var builder strings.Builder
builder.WriteString("map[string]any{")
for index, key := range keys {
if index > 0 {
builder.WriteString(", ")
}
val := assertedMap[key]
builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val)))
}
builder.WriteString("}")
return builder.String()
}
func formatNestedValue(value any) string {
switch val := value.(type) {
case string:
return fmt.Sprintf("%q", val)
case bool:
return fmt.Sprintf("%t", val)
case int, int64, float64:
return fmt.Sprintf("%v", val)
case map[string]any:
return toMapLiteral(val)
case []any:
var sliceBuilder strings.Builder
sliceBuilder.WriteString("[]any{")
for index, elem := range val {
if index > 0 {
sliceBuilder.WriteString(", ")
}
sliceBuilder.WriteString(formatNestedValue(elem))
}
sliceBuilder.WriteString("}")
return sliceBuilder.String()
default:
jsonBytes, err := json.Marshal(val)
if err != nil {
return "nil"
}
return fmt.Sprintf("%q", string(jsonBytes))
}
}
func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"SupportImports": supportImports,
"OpenFeatureType": openFeatureType,
"TypeString": typeString,
"ToMapLiteral": toMapLiteral,
}
newParams := &generators.Params[any]{
@ -80,8 +146,6 @@ func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for Go.
func NewGenerator(fs *flagset.Flagset) *GolangGenerator {
return &GolangGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -15,6 +15,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext
type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error)
type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error)
type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error)
type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error)
var client openfeature.IClient = nil
@ -29,11 +31,11 @@ var {{ .Key | ToPascal }} = struct {
// the evaluation error, if any, and the evaluation details.
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
}{
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{ .Type | TypeString }}, error) {
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{- if eq (.Type | OpenFeatureType) "Object"}}any{{- else}}{{ .Type | TypeString }}{{- end}}, error) {
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{ .Type | OpenFeatureType }}EvaluationDetails, error){
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{- if eq (.Type | OpenFeatureType) "Object"}}Interface{{- else }}{{ .Type | OpenFeatureType }}{{- end}}EvaluationDetails, error){
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
},
}
{{- end}}

View File

@ -0,0 +1,152 @@
package java
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type JavaGenerator struct {
generators.CommonGenerator
}
type Params struct {
// Add Java parameters here if needed
JavaPackage string
}
//go:embed java.tmpl
var javaTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
return "Integer"
case flagset.FloatType:
return "Double" //using Double as per openfeature Java-SDK
case flagset.BoolType:
return "Boolean"
case flagset.StringType:
return "String"
case flagset.ObjectType:
return "Object"
default:
return ""
}
}
func formatDefaultValueForJava(flag flagset.Flag) string {
switch flag.Type {
case flagset.StringType:
return fmt.Sprintf("\"%s\"", flag.DefaultValue)
case flagset.BoolType:
if flag.DefaultValue == true {
return "true"
}
return "false"
default:
return fmt.Sprintf("%v", flag.DefaultValue)
}
}
func toMapLiteral(value any) string {
assertedMap, ok := value.(map[string]any)
if !ok {
return "null"
}
keys := slices.Sorted(maps.Keys(assertedMap))
var builder strings.Builder
builder.WriteString("Map.of(")
for index, key := range keys {
if index > 0 {
builder.WriteString(", ")
}
val := assertedMap[key]
builder.WriteString(fmt.Sprintf("%q, %s", key, formatNestedValue(val)))
}
builder.WriteString(")")
return builder.String()
}
func formatNestedValue(value any) string {
switch val := value.(type) {
case string:
flag := flagset.Flag{
Type: flagset.StringType,
DefaultValue: val,
}
return formatDefaultValueForJava(flag)
case bool:
flag := flagset.Flag{
Type: flagset.BoolType,
DefaultValue: val,
}
return formatDefaultValueForJava(flag)
case int, int64:
flag := flagset.Flag{
Type: flagset.IntType,
DefaultValue: val,
}
return formatDefaultValueForJava(flag)
case float64:
flag := flagset.Flag{
Type: flagset.FloatType,
DefaultValue: val,
}
return formatDefaultValueForJava(flag)
case map[string]any:
return toMapLiteral(val)
case []any:
var sliceBuilder strings.Builder
sliceBuilder.WriteString("List.of(")
for index, elem := range val {
if index > 0 {
sliceBuilder.WriteString(", ")
}
sliceBuilder.WriteString(formatNestedValue(elem))
}
sliceBuilder.WriteString(")")
return sliceBuilder.String()
default:
jsonBytes, err := json.Marshal(val)
if err != nil {
return "null"
}
return fmt.Sprintf("%q", string(jsonBytes))
}
}
func (g *JavaGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"FormatDefaultValue": formatDefaultValueForJava,
"ToMapLiteral": toMapLiteral,
}
newParams := &generators.Params[any]{
OutputPath: params.OutputPath,
Custom: params.Custom,
}
return g.GenerateFile(funcs, javaTmpl, newParams, "OpenFeature.java")
}
// NewGenerator creates a generator for Java.
func NewGenerator(fs *flagset.Flagset) *JavaGenerator {
return &JavaGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,64 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
package {{ .Params.Custom.JavaPackage }};
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.OpenFeatureAPI;
public final class OpenFeature {
private OpenFeature() {} // prevent instantiation
public interface GeneratedClient {
{{ range .Flagset.Flags }}
/**
* {{ .Description }}
* Details:
* - Flag key: {{ .Key }}
* - Type: {{ .Type | OpenFeatureType }}
* - Default value: {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ .DefaultValue }}{{ end }}
* Returns the flag value
*/
{{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx);
/**
* {{ .Description }}
* Details:
* - Flag key: {{ .Key }}
* - Type: {{ .Type | OpenFeatureType }}
* - Default value: {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ .DefaultValue }}{{ end }}
* Returns the evaluation details containing the flag value and metadata
*/
FlagEvaluationDetails<{{ .Type | OpenFeatureType }}> {{ .Key | ToCamel }}Details(EvaluationContext ctx);
{{ end }}
}
private static final class OpenFeatureGeneratedClient implements GeneratedClient {
private final Client client;
private OpenFeatureGeneratedClient(Client client) {
this.client = client;
}
{{ range .Flagset.Flags }}
@Override
public {{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx) {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, ctx);
}
@Override
public FlagEvaluationDetails<{{ .Type | OpenFeatureType }}> {{ .Key | ToCamel }}Details(EvaluationContext ctx) {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, ctx);
}
{{ end }}
}
public static GeneratedClient getClient() {
return new OpenFeatureGeneratedClient(OpenFeatureAPI.getInstance().getClient());
}
public static GeneratedClient getClient(String domain) {
return new OpenFeatureGeneratedClient(OpenFeatureAPI.getInstance().getClient(domain));
}
}

View File

@ -12,10 +12,10 @@ type GeneratorCreator func() *cobra.Command
// GeneratorInfo contains metadata about a generator
type GeneratorInfo struct {
Name string
Description string
Stability Stability
Creator GeneratorCreator
Name string
Description string
Stability Stability
Creator GeneratorCreator
}
// GeneratorManager maintains a registry of available generators
@ -34,10 +34,10 @@ func NewGeneratorManager() *GeneratorManager {
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
cmd := cmdCreator()
m.generators[cmd.Use] = GeneratorInfo{
Name: cmd.Use,
Description: cmd.Short,
Stability: Stability(cmd.Annotations["stability"]),
Creator: cmdCreator,
Name: cmd.Use,
Description: cmd.Short,
Stability: Stability(cmd.Annotations["stability"]),
Creator: cmdCreator,
}
}

View File

@ -0,0 +1,66 @@
package nestjs
import (
_ "embed"
"encoding/json"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type NestJsGenerator struct {
generators.CommonGenerator
}
type Params struct {
}
//go:embed nestjs.tmpl
var nestJsTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
fallthrough
case flagset.FloatType:
return "number"
case flagset.BoolType:
return "boolean"
case flagset.StringType:
return "string"
case flagset.ObjectType:
return "object"
default:
return ""
}
}
func toJSONString(value any) string {
bytes, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(bytes)
}
func (g *NestJsGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
}
newParams := &generators.Params[any]{
OutputPath: params.OutputPath,
Custom: Params{},
}
return g.GenerateFile(funcs, nestJsTmpl, newParams, "openfeature-decorators.ts")
}
// NewGenerator creates a generator for NestJS.
func NewGenerator(fs *flagset.Flagset) *NestJsGenerator {
return &NestJsGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,121 @@
import type { DynamicModule, FactoryProvider as NestFactoryProvider } from "@nestjs/common";
import { Inject, Module } from "@nestjs/common";
import type { Observable } from "rxjs";
import type {
OpenFeature,
Client,
EvaluationContext,
EvaluationDetails,
OpenFeatureModuleOptions,
JsonValue
} from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk";
import type { GeneratedClient } from "./openfeature";
import { getGeneratedClient } from "./openfeature";
/**
* Returns an injection token for a (domain scoped) generated OpenFeature client.
* @param {string} domain The domain of the generated OpenFeature client.
* @returns {string} The injection token.
*/
export function getOpenFeatureGeneratedClientToken(domain?: string): string {
return domain ? `OpenFeatureGeneratedClient_${domain}` : "OpenFeatureGeneratedClient_default";
}
/**
* Options for injecting an OpenFeature client into a constructor.
*/
interface FeatureClientProps {
/**
* The domain of the OpenFeature client, if a domain scoped client should be used.
* @see {@link Client.getBooleanDetails}
*/
domain?: string;
}
/**
* Injects a generated typesafe feature client into a constructor or property of a class.
* @param {FeatureClientProps} [props] The options for injecting the client.
* @returns {PropertyDecorator & ParameterDecorator} The decorator function.
*/
export const GeneratedOpenFeatureClient = (props?: FeatureClientProps): PropertyDecorator & ParameterDecorator =>
Inject(getOpenFeatureGeneratedClientToken(props?.domain));
/**
* GeneratedOpenFeatureModule is a generated typesafe NestJS wrapper for OpenFeature Server-SDK.
*/
@Module({})
export class GeneratedOpenFeatureModule extends OpenFeatureModule {
static override forRoot({ useGlobalInterceptor = true, ...options }: OpenFeatureModuleOptions): DynamicModule {
const module = super.forRoot({ useGlobalInterceptor, ...options });
const clientValueProviders: NestFactoryProvider<GeneratedClient>[] = [
{
provide: getOpenFeatureGeneratedClientToken(),
useFactory: () => getGeneratedClient(),
},
];
if (options?.providers) {
const domainClientProviders: NestFactoryProvider<GeneratedClient>[] = Object.keys(options.providers).map(
(domain) => ({
provide: getOpenFeatureGeneratedClientToken(domain),
useFactory: () => getGeneratedClient(domain),
}),
);
clientValueProviders.push(...domainClientProviders);
}
return {
...module,
providers: module.providers ? [...module.providers, ...clientValueProviders] : clientValueProviders,
exports: module.exports ? [...module.exports, ...clientValueProviders] : clientValueProviders,
};
}
}
/**
* Options for injecting a typed feature flag into a route handler.
*/
interface TypedFeatureProps {
/**
* The domain of the OpenFeature client, if a domain scoped client should be used.
* @see {@link OpenFeature#getClient}
*/
domain?: string;
/**
* The {@link EvaluationContext} for evaluating the feature flag.
* @see {@link OpenFeature#getClient}
*/
context?: EvaluationContext;
}
{{ range .Flagset.Flags }}
/**
* Gets the {@link EvaluationDetails} for `{{ .Key }}` from a domain scoped or the default OpenFeature
* client and populates the annotated parameter with the {@link EvaluationDetails} wrapped in an {@link Observable}.
*
* **Details:**
* - flag key: `{{ .Key }}`
* - description: `{{ .Description }}`
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @{{ .Key | ToPascal }}()
* {{ .Key | ToCamel }}: Observable<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
* @returns {ParameterDecorator} The decorator function.
*/
export function {{ .Key | ToPascal }}(props?: TypedFeatureProps): ParameterDecorator {
return {{ .Type | OpenFeatureType | ToPascal }}FeatureFlag({ flagKey: {{ .Key | Quote }}, defaultValue: {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, ...props });
}
{{ end -}}

View File

@ -2,6 +2,7 @@ package nodejs
import (
_ "embed"
"encoding/json"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
return "boolean"
case flagset.StringType:
return "string"
case flagset.ObjectType:
return "object"
default:
return ""
}
}
func toJSONString(value any) string {
bytes, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(bytes)
}
func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
}
newParams := &generators.Params[any]{
@ -49,8 +61,6 @@ func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for NodeJS.
func NewGenerator(fs *flagset.Flagset) *NodejsGenerator {
return &NodejsGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -3,6 +3,7 @@ import {
OpenFeature,
stringOrUndefined,
objectOrUndefined,
JsonValue,
} from "@openfeature/server-sdk";
import type {
EvaluationContext,
@ -17,30 +18,30 @@ export interface GeneratedClient {
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
*
* Performs a flag evaluation that returns a {{ .Type | OpenFeatureType }}.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<{{ .Type | OpenFeatureType }}>} Flag evaluation response
* @returns {Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>} Flag evaluation response
*/
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}>;
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>;
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
*
* Performs a flag evaluation that a returns an evaluation details object.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>} Flag evaluation details response
* @returns {Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>} Flag evaluation details response
*/
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>;
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>;
{{ end -}}
}
@ -74,12 +75,12 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
return {
{{- range .Flagset.Flags }}
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}> => {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options);
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> => {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
},
{{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>> => {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options);
{{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>> => {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
},
{{ end -}}
{{ printf " " }}}

View File

@ -0,0 +1,165 @@
package python
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type PythonGenerator struct {
generators.CommonGenerator
}
type Params struct {
}
//go:embed python.tmpl
var pythonTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
return "int"
case flagset.FloatType:
return "float"
case flagset.BoolType:
return "bool"
case flagset.StringType:
return "str"
default:
return "object"
}
}
func methodType(flagType flagset.FlagType) string {
switch flagType {
case flagset.StringType:
return "string"
case flagset.IntType:
return "integer"
case flagset.BoolType:
return "boolean"
case flagset.FloatType:
return "float"
case flagset.ObjectType:
return "object"
default:
panic("unsupported flag type")
}
}
func typedGetMethodSync(flagType flagset.FlagType) string {
return "get_" + methodType(flagType) + "_value"
}
func typedGetMethodAsync(flagType flagset.FlagType) string {
return "get_" + methodType(flagType) + "_value_async"
}
func typedDetailsMethodSync(flagType flagset.FlagType) string {
return "get_" + methodType(flagType) + "_details"
}
func typedDetailsMethodAsync(flagType flagset.FlagType) string {
return "get_" + methodType(flagType) + "_details_async"
}
func pythonBoolLiteral(value any) any {
if v, ok := value.(bool); ok {
if v {
return "True"
}
return "False"
}
return value
}
func toPythonDict(value any) string {
assertedMap, ok := value.(map[string]any)
if !ok {
return "None"
}
// To have a determined order of the object for comparison
keys := slices.Sorted(maps.Keys(assertedMap))
var builder strings.Builder
builder.WriteString("{")
for index, key := range keys {
if index != 0 {
builder.WriteString(", ")
}
val := assertedMap[key]
builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val)))
}
builder.WriteString("}")
return builder.String()
}
func formatNestedValue(value any) string {
switch val := value.(type) {
case string:
return fmt.Sprintf("%q", val)
case bool:
return fmt.Sprintf(pythonBoolLiteral(val).(string))
case int, int64, float64:
return fmt.Sprintf("%v", val)
case map[string]any:
return toPythonDict(val)
case []any:
var sliceBuilder strings.Builder
sliceBuilder.WriteString("[")
for index, elem := range val {
if index > 0 {
sliceBuilder.WriteString(", ")
}
sliceBuilder.WriteString(formatNestedValue(elem))
}
sliceBuilder.WriteString("]")
return sliceBuilder.String()
default:
jsonBytes, err := json.Marshal(val)
if err != nil {
return "None"
}
return strings.ReplaceAll(string(jsonBytes), "null", "None")
}
}
func (g *PythonGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"TypedGetMethodSync": typedGetMethodSync,
"TypedGetMethodAsync": typedGetMethodAsync,
"TypedDetailsMethodSync": typedDetailsMethodSync,
"TypedDetailsMethodAsync": typedDetailsMethodAsync,
"PythonBoolLiteral": pythonBoolLiteral,
"ToPythonDict": toPythonDict,
}
newParams := &generators.Params[any]{
OutputPath: params.OutputPath,
Custom: Params{},
}
return g.GenerateFile(funcs, pythonTmpl, newParams, "openfeature.py")
}
// NewGenerator creates a generator for Python.
func NewGenerator(fs *flagset.Flagset) *PythonGenerator {
return &PythonGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,121 @@
# AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
from typing import Optional
from openfeature.client import OpenFeatureClient
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagEvaluationOptions
from openfeature.hook import Hook
class GeneratedClient:
def __init__(
self,
client: OpenFeatureClient,
) -> None:
self.client = client
{{ printf "" }}
{{- range .Flagset.Flags }}
def {{ .Key | ToSnake }}(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> {{ .Type | OpenFeatureType }}:
"""
{{ .Description }}
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}`
- type: `{{ .Type | OpenFeatureType }}`
Performs a flag evaluation that returns a `{{ .Type | OpenFeatureType }}`.
"""
return self.client.{{ .Type | TypedGetMethodSync }}(
flag_key={{ .Key | Quote }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
def {{ .Key | ToSnake }}_details(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
{{ .Description }}
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}`
- type: `{{ .Type | OpenFeatureType }}`
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
"""
return self.client.{{ .Type | TypedDetailsMethodSync }}(
flag_key={{ .Key | Quote }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def {{ .Key | ToSnake }}_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> {{ .Type | OpenFeatureType }}:
"""
{{ .Description }}
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}`
- type: `{{ .Type | OpenFeatureType }}`
Performs a flag evaluation asynchronously and returns a `{{ .Type | OpenFeatureType }}`.
"""
return await self.client.{{ .Type | TypedGetMethodAsync }}(
flag_key={{ .Key | Quote }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
async def {{ .Key | ToSnake }}_details_async(
self,
evaluation_context: Optional[EvaluationContext] = None,
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
) -> FlagEvaluationDetails:
"""
{{ .Description }}
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | PythonBoolLiteral }}{{- end }}`
- type: `{{ .Type | OpenFeatureType }}`
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
"""
return await self.client.{{ .Type | TypedDetailsMethodAsync }}(
flag_key={{ .Key | Quote }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
{{ end -}}
{{ printf "\n" }}
def get_generated_client(
client: Optional[OpenFeatureClient] = None,
domain: Optional[str] = None,
version: Optional[str] = None,
context: Optional[EvaluationContext] = None,
hooks: Optional[list[Hook]] = None,
) -> GeneratedClient:
if not client:
client = OpenFeatureClient(
domain=domain,
version=version,
context=context,
hooks=hooks,
)
return GeneratedClient(client)

View File

@ -2,6 +2,7 @@ package react
import (
_ "embed"
"encoding/json"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
return "boolean"
case flagset.StringType:
return "string"
case flagset.ObjectType:
return "object"
default:
return ""
}
}
func toJSONString(value any) string {
bytes, err := json.Marshal(value)
if err != nil {
return "{}"
}
return string(bytes)
}
func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
}
newParams := &generators.Params[any]{
@ -49,8 +61,6 @@ func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for React.
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
return &ReactGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -5,6 +5,7 @@ import {
type ReactFlagEvaluationNoSuspenseOptions,
useFlag,
useSuspenseFlag,
JsonValue
} from "@openfeature/react-sdk";
{{ range .Flagset.Flags }}
/**
@ -12,11 +13,11 @@ import {
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
*/
export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => {
return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
return useFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
};
/**
@ -24,13 +25,13 @@ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) =
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
*
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
return useSuspenseFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
};
{{ end}}

View File

@ -0,0 +1,65 @@
package manifest
import (
"reflect"
"sort"
"testing"
)
func TestCompareDifferentManifests(t *testing.T) {
oldManifest := &Manifest{
Flags: map[string]any{
"flag1": "value1",
"flag2": "value2",
},
}
newManifest := &Manifest{
Flags: map[string]any{
"flag1": "value1",
"flag2": "newValue2",
"flag3": "value3",
},
}
changes, err := Compare(oldManifest, newManifest)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedChanges := []Change{
{Type: "change", Path: "flags.flag2", OldValue: "value2", NewValue: "newValue2"},
{Type: "add", Path: "flags.flag3", NewValue: "value3"},
}
sortChanges(changes)
sortChanges(expectedChanges)
if !reflect.DeepEqual(changes, expectedChanges) {
t.Errorf("expected %v, got %v", expectedChanges, changes)
}
}
func TestCompareIdenticalManifests(t *testing.T) {
manifest := &Manifest{
Flags: map[string]any{
"flag1": "value1",
"flag2": "value2",
},
}
changes, err := Compare(manifest, manifest)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(changes) != 0 {
t.Errorf("expected no changes, got %v", changes)
}
}
func sortChanges(changes []Change) {
sort.Slice(changes, func(i, j int) bool {
return changes[i].Path < changes[j].Path
})
}

View File

@ -63,9 +63,9 @@ type Manifest struct {
// Converts the Manifest struct to a JSON schema.
func ToJSONSchema() *jsonschema.Schema {
reflector := &jsonschema.Reflector{
ExpandedStruct: true,
ExpandedStruct: true,
AllowAdditionalProperties: true,
BaseSchemaID: "openfeature-cli",
BaseSchemaID: "openfeature-cli",
}
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/open-feature/cli/internal/filesystem"
"github.com/spf13/afero"
)
type initManifest struct {
@ -14,7 +15,7 @@ type initManifest struct {
// Create creates a new manifest file at the given path.
func Create(path string) error {
m := &initManifest{
Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
Schema: "https://raw.githubusercontent.com/open-feature/cli/main/schema/v0/flag-manifest.json",
Manifest: Manifest{
Flags: map[string]any{},
},
@ -25,3 +26,19 @@ func Create(path string) error {
}
return filesystem.WriteFile(path, formattedInitManifest)
}
// Load loads a manifest from a JSON file, unmarshals it, and returns a Manifest object.
func Load(path string) (*Manifest, error) {
fs := filesystem.FileSystem()
data, err := afero.ReadFile(fs, path)
if err != nil {
return nil, err
}
var m Manifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
}
return &m, nil
}

View File

@ -0,0 +1,35 @@
package manifest
// OutputFormat represents the available output formats for the compare command
type OutputFormat string
const (
// OutputFormatTree represents the tree output format (default)
OutputFormatTree OutputFormat = "tree"
// OutputFormatFlat represents the flat output format
OutputFormatFlat OutputFormat = "flat"
// OutputFormatJSON represents the JSON output format
OutputFormatJSON OutputFormat = "json"
// OutputFormatYAML represents the YAML output format
OutputFormatYAML OutputFormat = "yaml"
)
// IsValidOutputFormat checks if the given format is a valid output format
func IsValidOutputFormat(format string) bool {
switch OutputFormat(format) {
case OutputFormatTree, OutputFormatFlat, OutputFormatJSON, OutputFormatYAML:
return true
default:
return false
}
}
// GetValidOutputFormats returns a list of all valid output formats
func GetValidOutputFormats() []string {
return []string{
string(OutputFormatTree),
string(OutputFormatFlat),
string(OutputFormatJSON),
string(OutputFormatYAML),
}
}

21
lefthook.yml Normal file
View File

@ -0,0 +1,21 @@
# This file configures Lefthook, a Git hooks manager, for the project.
# For detailed instructions on how to contribute and set up Lefthook,
# please refer to the relevant section in the contributing documentation (CONTRIBUTING.md).
pre-commit:
commands:
go-fmt:
run: go fmt ./...
stage_fixed: true
pre-push:
commands:
generate-docs:
run: |
make generate-docs
if ! git diff --quiet; then
echo "Documentation is outdated. Please run 'make generate-docs' and commit the changes."
exit 1
fi
skip: false
tests:
run: make test
skip: false

4
renovate.json Normal file
View File

@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>open-feature/community-tooling"]
}

View File

@ -29,7 +29,7 @@ func main() {
defer file.Close()
if _, err := file.Write(data); err != nil {
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err));
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err))
}
fmt.Println("JSON schema generated successfully at " + schemaPath)

View File

@ -49,11 +49,11 @@ func walkPath(shouldPass bool, root string) error {
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
manifestLoader := gojsonschema.NewGoLoader(v)
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
if (err != nil) {
if err != nil {
return fmt.Errorf("Error validating json schema: %v", err)
}
if (len(result.Errors()) >= 1 && shouldPass == true) {
if len(result.Errors()) >= 1 && shouldPass == true {
var errorMessage strings.Builder
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
@ -63,7 +63,7 @@ func walkPath(shouldPass bool, root string) error {
return fmt.Errorf("%s", errorMessage.String())
}
if (len(result.Errors()) == 0 && shouldPass == false) {
if len(result.Errors()) == 0 && shouldPass == false {
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
}

65
test/README.md Normal file
View File

@ -0,0 +1,65 @@
# OpenFeature CLI Integration Testing
This directory contains integration tests for validating the OpenFeature CLI generators.
## Integration Test Structure
The integration tests use [Dagger](https://dagger.io/) to create reproducible test environments without needing to install dependencies locally.
Each integration test:
1. Builds the CLI from source
2. Generates code using a sample manifest file
3. Compiles and tests the generated code in a language-specific container
4. Reports success or failure
## Running Tests
### Run all integration tests
```bash
make test-integration
```
### Run a specific integration test
```bash
# For C# tests
make test-csharp-dagger
```
## Adding a New Integration Test
To add an integration test for a new generator:
1. Create a combined implementation and runner file in `test/integration/cmd/<language>/run.go`
2. Update the main runner in `test/integration/cmd/run.go` to execute your new test
3. Add a Makefile target for running your test individually
See the step-by-step guide in [new-language.md](new-language.md) for detailed instructions.
## How It Works
The testing framework uses the following components:
- `test/integration/integration.go`: Defines the `Test` interface and common utilities
- `test/integration/cmd/run.go`: Runner for all integration tests that executes each language-specific test
- `test/integration/cmd/<language>/run.go`: Combined implementation and runner for each language
- `test/<language>-integration/`: Contains language-specific test files (code samples, project files)
Each integration test uses Dagger to:
1. Build the CLI in a clean environment
2. Generate code using a sample manifest
3. Compile and test the generated code in a language-specific container
4. Report success or failure
## Benefits Over Shell Scripts
Using Dagger for integration tests provides several advantages:
1. **Reproducibility**: Tests run in containerized environments that are identical locally and in CI
2. **Language Support**: Easy to add new language tests with the same pattern
3. **Improved Debugging**: Clear separation of build, generate, and test steps
4. **Parallelization**: Tests can run in parallel when executed in different containers
5. **No Dependencies**: No need to install language-specific tooling locally

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="OpenFeature" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="generated\*.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
# Copy necessary files
COPY expected/OpenFeature.cs /app/
COPY CompileTest.csproj /app/
COPY Program.cs /app/
# Restore dependencies
RUN dotnet restore
# Build the project
RUN dotnet build
# The image will be used to validate C# compilation only
ENTRYPOINT ["dotnet", "run"]

View File

@ -0,0 +1,208 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenFeature;
using OpenFeature.Model;
namespace OpenFeature
{
/// <summary>
/// Generated OpenFeature client for typesafe flag access
/// </summary>
public class GeneratedClient
{
private readonly IFeatureClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
/// </summary>
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
public GeneratedClient(IFeatureClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: float</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<float> DiscountPercentageAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetFloatValueAsync("discountPercentage", 0.15, evaluationContext);
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: float</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<EvaluationDetails<float>> DiscountPercentageDetailsAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetFloatDetailsAsync("discountPercentage", 0.15, evaluationContext);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<bool> EnableFeatureAAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetBoolValueAsync("enableFeatureA", false, evaluationContext);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<EvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetBoolDetailsAsync("enableFeatureA", false, evaluationContext);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<string> GreetingMessageAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<EvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<int> UsernameMaxLengthAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetIntValueAsync("usernameMaxLength", 50, evaluationContext);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<EvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext evaluationContext = null)
{
return await _client.GetIntDetailsAsync("usernameMaxLength", 50, evaluationContext);
}
/// <summary>
/// Creates a new GeneratedClient using the default OpenFeature client
/// </summary>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient()
{
return new GeneratedClient(Api.GetClient());
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain)
{
return new GeneratedClient(Api.GetClient(domain));
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <param name="evaluationContext">Default context to use for evaluations</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain, EvaluationContext evaluationContext)
{
return new GeneratedClient(Api.GetClient(domain, evaluationContext));
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using OpenFeature;
using OpenFeature.Model;
using TestNamespace;
// This program just validates that the generated OpenFeature C# client code compiles
// We don't need to run the code since the goal is to test compilation only
namespace CompileTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Testing compilation of generated OpenFeature client...");
// Test DI initialization
var services = new ServiceCollection();
// Register OpenFeature services manually for the test
services.AddSingleton(_ => Api.Instance);
services.AddSingleton<IFeatureClient>(_ => Api.Instance.GetClient());
services.AddSingleton<GeneratedClient>();
var serviceProvider = services.BuildServiceProvider();
// Test client retrieval from DI
var client = serviceProvider.GetRequiredService<GeneratedClient>();
// Also test the traditional factory method
var clientFromFactory = GeneratedClient.CreateClient();
// Success!
Console.WriteLine("Generated C# code compiles successfully!");
}
}
}

View File

@ -0,0 +1,43 @@
# C# Integration Testing
This directory contains integration tests for the C# code generator.
## Running the tests
Run the C# integration tests with Dagger:
```bash
make test-csharp-dagger
```
This will:
1. Build the OpenFeature CLI
2. Generate C# client code using the sample manifest
3. Run the C# compilation test in an isolated environment
4. Report success or failure
## What the test does
The integration test:
1. Builds the OpenFeature CLI inside a container
2. Generates C# client code using a sample manifest
3. Compiles the generated code with a sample program
4. Runs the compiled program to verify it works correctly
## Test Files
- `CompileTest.csproj`: .NET project file for compilation testing
- `Program.cs`: Test program that uses the generated code
- `expected/`: Directory containing expected output files (used for verification)
## Implementation
The C# integration test uses Dagger to create a reproducible test environment:
1. It builds the CLI in a Go container
2. Generates C# code using the CLI
3. Tests the generated code in a .NET container
The implementation is located in `test/integration/cmd/csharp/run.go`.
For more implementation details, see the main [test/README.md](../README.md) file.

View File

@ -0,0 +1,252 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using OpenFeature;
using OpenFeature.Model;
namespace TestNamespace
{
/// <summary>
/// Service collection extensions for OpenFeature
/// </summary>
public static class OpenFeatureServiceExtensions
{
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient())
.AddSingleton<GeneratedClient>();
}
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client for a specific domain
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <param name="domain">The domain to get the client for</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient(domain))
.AddSingleton<GeneratedClient>();
}
}
/// <summary>
/// Generated OpenFeature client for typesafe flag access
/// </summary>
public class GeneratedClient
{
private readonly IFeatureClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
/// </summary>
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
public GeneratedClient(IFeatureClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<double> DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<double>> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<bool> EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<string> GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<int> UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Creates a new GeneratedClient using the default OpenFeature client
/// </summary>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient()
{
return new GeneratedClient(Api.Instance.GetClient());
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <param name="evaluationContext">Default context to use for evaluations</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
}
}

View File

@ -0,0 +1,251 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using OpenFeature;
using OpenFeature.Model;
namespace TestNamespace
{
/// <summary>
/// Service collection extensions for OpenFeature
/// </summary>
public static class OpenFeatureServiceExtensions
{
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient())
.AddSingleton<GeneratedClient>();
}
/// <summary>
/// Adds OpenFeature services to the service collection with the generated client for a specific domain
/// </summary>
/// <param name="services">The service collection to add services to</param>
/// <param name="domain">The domain to get the client for</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
{
return services
.AddSingleton(_ => Api.Instance)
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient(domain))
.AddSingleton<GeneratedClient>();
}
}
/// <summary>
/// Generated OpenFeature client for typesafe flag access
/// </summary>
public class GeneratedClient
{
private readonly IFeatureClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
/// </summary>
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
public GeneratedClient(IFeatureClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<double> DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Discount percentage applied to purchases.
/// </summary>
/// <remarks>
/// <para>Flag key: discountPercentage</para>
/// <para>Default value: 0.15</para>
/// <para>Type: double</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<double>> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<bool> EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// Controls whether Feature A is enabled.
/// </summary>
/// <remarks>
/// <para>Flag key: enableFeatureA</para>
/// <para>Default value: false</para>
/// <para>Type: bool</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<string> GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// The message to use for greeting users.
/// </summary>
/// <remarks>
/// <para>Flag key: greetingMessage</para>
/// <para>Default value: Hello there!</para>
/// <para>Type: string</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Allows customization of theme colors.
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
/// <para>Type: object</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The flag value</returns>
public async Task<int> UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Maximum allowed length for usernames.
/// </summary>
/// <remarks>
/// <para>Flag key: usernameMaxLength</para>
/// <para>Default value: 50</para>
/// <para>Type: int</para>
/// </remarks>
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
/// <param name="options">Options for flag evaluation</param>
/// <returns>The evaluation details containing the flag value and metadata</returns>
public async Task<FlagEvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
{
return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
}
/// <summary>
/// Creates a new GeneratedClient using the default OpenFeature client
/// </summary>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient()
{
return new GeneratedClient(Api.Instance.GetClient());
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
/// <summary>
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
/// </summary>
/// <param name="domain">The domain to get the client for</param>
/// <param name="evaluationContext">Default context to use for evaluations</param>
/// <returns>A new GeneratedClient instance</returns>
public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
{
return new GeneratedClient(Api.Instance.GetClient(domain));
}
}
}

View File

@ -0,0 +1,13 @@
module github.com/open-feature/cli/test/go-integration
go 1.22
require github.com/open-feature/go-sdk v1.15.0
require (
github.com/go-logr/logr v1.4.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
golang.org/x/net v0.21.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)

106
test/go-integration/test.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"context"
"fmt"
"os"
generated "github.com/open-feature/cli/test/go-integration/openfeature"
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run() error {
// Set up the in-memory provider with test flags
provider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
"discountPercentage": {
State: memprovider.Enabled,
DefaultVariant: "default",
Variants: map[string]any{
"default": 0.15,
},
},
"enableFeatureA": {
State: memprovider.Enabled,
DefaultVariant: "default",
Variants: map[string]any{
"default": false,
},
},
"greetingMessage": {
State: memprovider.Enabled,
DefaultVariant: "default",
Variants: map[string]any{
"default": "Hello there!",
},
},
"usernameMaxLength": {
State: memprovider.Enabled,
DefaultVariant: "default",
Variants: map[string]any{
"default": 50,
},
},
"themeCustomization": {
State: memprovider.Enabled,
DefaultVariant: "default",
Variants: map[string]any{
"default": map[string]any{
"primaryColor": "#007bff",
"secondaryColor": "#6c757d",
},
},
},
})
// Set the provider and wait for it to be ready
err := openfeature.SetProviderAndWait(provider)
if err != nil {
return fmt.Errorf("Failed to set provider: %w", err)
}
ctx := context.Background()
evalCtx := openfeature.NewEvaluationContext("someid", map[string]any{})
// Use the generated code for all flag evaluations
enableFeatureA, err := generated.EnableFeatureA.Value(ctx, evalCtx)
if err != nil {
return fmt.Errorf("Error evaluating boolean flag: %w", err)
}
fmt.Printf("enableFeatureA: %v\n", enableFeatureA)
discount, err := generated.DiscountPercentage.Value(ctx, evalCtx)
if err != nil {
return fmt.Errorf("Failed to get discount: %w", err)
}
fmt.Printf("Discount Percentage: %.2f\n", discount)
greetingMessage, err := generated.GreetingMessage.Value(ctx, evalCtx)
if err != nil {
return fmt.Errorf("Error evaluating string flag: %w", err)
}
fmt.Printf("greetingMessage: %v\n", greetingMessage)
usernameMaxLength, err := generated.UsernameMaxLength.Value(ctx, evalCtx)
if err != nil {
return fmt.Errorf("Error evaluating int flag: %v\n", err)
}
fmt.Printf("usernameMaxLength: %v\n", usernameMaxLength)
themeCustomization, err := generated.ThemeCustomization.Value(ctx, evalCtx)
if err != nil {
return fmt.Errorf("Error evaluating int flag: %v\n", err)
}
fmt.Printf("themeCustomization: %v\n", themeCustomization)
fmt.Println("Generated Go code compiles successfully!")
return nil
}

View File

@ -0,0 +1,97 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"dagger.io/dagger"
"github.com/open-feature/cli/test/integration"
)
// Test implements the integration test for the C# generator
type Test struct {
// ProjectDir is the absolute path to the root of the project
ProjectDir string
// TestDir is the absolute path to the test directory
TestDir string
}
// New creates a new Test
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}
// Run executes the C# integration test using Dagger
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Source code container
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"CompileTest.csproj", "Program.cs"},
})
// Build the CLI
cli := client.Container().
From("golang:1.24-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
// Generate C# client
generated := cli.WithExec([]string{
"./cli", "generate", "csharp",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
"--namespace=TestNamespace",
})
// Get generated files
generatedFiles := generated.Directory("/tmp/generated")
// Test C# compilation with the generated files
dotnetContainer := client.Container().
From("mcr.microsoft.com/dotnet/sdk:8.0").
WithDirectory("/app/generated", generatedFiles).
WithDirectory("/app", testFiles).
WithWorkdir("/app").
WithExec([]string{"dotnet", "restore"}).
WithExec([]string{"dotnet", "build"}).
WithExec([]string{"dotnet", "run"})
return dotnetContainer, nil
}
// Name returns the name of the integration test
func (t *Test) Name() string {
return "csharp"
}
func main() {
ctx := context.Background()
// Get project root
projectDir, err := filepath.Abs(os.Getenv("PWD"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}
// Get test directory
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/csharp-integration"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
os.Exit(1)
}
// Create and run the C# integration test
test := New(projectDir, testDir)
if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,101 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"dagger.io/dagger"
"github.com/open-feature/cli/test/integration"
)
// Test implements the integration test for the Go generator
type Test struct {
// ProjectDir is the absolute path to the root of the project
ProjectDir string
// TestDir is the absolute path to the test directory
TestDir string
}
// New creates a new Test
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}
// Run executes the Go integration test using Dagger
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Source code container
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"test.go", "go.mod"},
})
// Build the CLI
cli := client.Container().
From("golang:1.23-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "mod", "tidy"}).
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
// Generate Go client
generated := cli.WithExec([]string{
"./cli", "generate", "go",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
"--package-name=openfeature",
})
// Get generated files
generatedFiles := generated.Directory("/tmp/generated")
// Test Go compilation with the generated files
goContainer := client.Container().
From("golang:1.23-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithWorkdir("/app").
WithDirectory("/app", testFiles).
WithDirectory("/app/openfeature", generatedFiles).
WithExec([]string{"go", "mod", "tidy"}).
WithExec([]string{"go", "build", "-o", "test", "-v"}).
WithExec([]string{"./test"})
return goContainer, nil
}
// Name returns the name of the integration test
func (t *Test) Name() string {
return "go"
}
func main() {
ctx := context.Background()
// Get project root
projectDir, err := filepath.Abs(os.Getenv("PWD"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}
// Get test directory
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/go-integration"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
os.Exit(1)
}
// Create and run the Go integration test
test := New(projectDir, testDir)
if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,77 @@
package main
import (
"context"
"dagger.io/dagger"
"fmt"
"github.com/open-feature/cli/test/integration"
"os"
"path/filepath"
)
type Test struct {
ProjectDir string
TestDir string
}
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"package.json", "test.ts"},
})
cli := client.Container().
From("golang:1.24-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
generated := cli.WithExec([]string{
"./cli", "generate", "nodejs",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
})
generatedFiles := generated.Directory("/tmp/generated")
nodeContainer := client.Container().
From("node:22-alpine").
WithExec([]string{"npm", "install", "-g", "typescript"}).
WithDirectory("/app/generated", generatedFiles).
WithDirectory("/app", testFiles).
WithWorkdir("/app").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "test"})
return nodeContainer, nil
}
func (t *Test) Name() string {
return "nodejs"
}
func main() {
ctx := context.Background()
projectDir, err := filepath.Abs(os.Getenv("PWD"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/nodejs-integration"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
os.Exit(1)
}
test := New(projectDir, testDir)
if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,43 @@
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// Run the language-specific tests
fmt.Println("=== Running all integration tests ===")
// Run the C# integration test
csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp")
csharpCmd.Stdout = os.Stdout
csharpCmd.Stderr = os.Stderr
if err := csharpCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err)
os.Exit(1)
}
// Run the Go integration test
goCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/go")
goCmd.Stdout = os.Stdout
goCmd.Stderr = os.Stderr
if err := goCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running Go integration test: %v\n", err)
os.Exit(1)
}
//Run the nodejs test
nodeCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/nodejs")
nodeCmd.Stdout = os.Stdout
nodeCmd.Stderr = os.Stderr
if err := nodeCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running nodejs integration test: %v\n", err)
os.Exit(1)
}
// Add more tests here as they are available
fmt.Println("=== All integration tests passed successfully ===")
}

View File

@ -0,0 +1,44 @@
package integration
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
// Test defines the interface for all integration tests
type Test interface {
// Run executes the integration test with the given Dagger client
Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error)
// Name returns the name of the integration test
Name() string
}
// RunTest runs a single integration test
func RunTest(ctx context.Context, test Test) error {
// Initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
return fmt.Errorf("failed to connect to Dagger engine: %w", err)
}
defer client.Close()
fmt.Printf("=== Running %s integration test ===\n", test.Name())
// Run the integration test
container, err := test.Run(ctx, client)
if err != nil {
return fmt.Errorf("failed to run %s integration test: %w", test.Name(), err)
}
// Execute the pipeline and wait for it to complete
_, err = container.Stdout(ctx)
if err != nil {
return fmt.Errorf("%s integration test failed: %w", test.Name(), err)
}
fmt.Printf("=== Success: %s integration test passed ===\n", test.Name())
return nil
}

190
test/new-generator.md Normal file
View File

@ -0,0 +1,190 @@
# Adding a New Generator Integration Test
This guide explains how to add integration tests for a new generator.
## Directory Structure
The integration testing framework has the following directory structure:
```
test/
integration/ # Core integration test framework
integration.go # Test interface definition
cmd/ # Command-line runners and implementations
run.go # Runner for all tests
csharp/ # C# specific implementation and runner
run.go
python/ # Python specific implementation and runner (future)
run.go
csharp-integration/ # C# test files
python-integration/ # Python test files (future)
```
## Step 1: Create a generator-specific implementation and runner
Create a file at `test/integration/cmd/python/run.go`:
```go
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"dagger.io/dagger"
"github.com/open-feature/cli/test/integration"
)
// Test implements the integration test for the Python generator
type Test struct {
ProjectDir string
TestDir string
}
// New creates a new Test
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}
// Run executes the Python integration test
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Source code container
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"test_openfeature.py", "requirements.txt"},
})
// Build the CLI
cli := client.Container().
From("golang:1.24-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "cli"})
// Generate Python client
generated := cli.WithExec([]string{
"./cli", "generate", "python",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
"--package=openfeature_test",
})
// Get generated files
generatedFiles := generated.Directory("/tmp/generated")
// Test Python with the generated files
pythonContainer := client.Container().
From("python:3.11-slim").
WithDirectory("/app/openfeature", generatedFiles).
WithDirectory("/app/test", testFiles).
WithWorkdir("/app").
WithExec([]string{"pip", "install", "-r", "test/requirements.txt"}).
WithExec([]string{"python", "-m", "pytest", "test/test_openfeature.py", "-v"})
return pythonContainer, nil
}
// Name returns the name of the integration test
func (t *Test) Name() string {
return "python"
}
func main() {
ctx := context.Background()
// Get project root
projectDir, err := filepath.Abs(os.Getenv("PWD"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}
// Get test directory
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/python-integration"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
os.Exit(1)
}
// Create and run the Python integration test
test := New(projectDir, testDir)
if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
```
## Step 2: Add the test to the all-integration runner
Update `test/integration/cmd/run.go` to include your test:
```go
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// Run the generator-specific tests
fmt.Println("=== Running all integration tests ===")
// Run the C# integration test
csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp")
csharpCmd.Stdout = os.Stdout
csharpCmd.Stderr = os.Stderr
if err := csharpCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err)
os.Exit(1)
}
// Run the Python integration test
pythonCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/python")
pythonCmd.Stdout = os.Stdout
pythonCmd.Stderr = os.Stderr
if err := pythonCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running Python integration test: %v\n", err)
os.Exit(1)
}
// Add more tests here as they are available
fmt.Println("=== All integration tests passed successfully ===")
}
```
## Step 3: Create test files
Create the following directory structure with your test files:
```
test/
python-integration/
requirements.txt
test_openfeature.py
README.md
```
## Step 4: Add a Makefile target
Update the Makefile with a new target:
```makefile
.PHONY: test-python-dagger
test-python-dagger:
@echo "Running Python integration test with Dagger..."
@go run ./test/integration/cmd/python/run.go
```
## Step 5: Update the documentation
Update `test/README.md` to include your new test.

598
test/nodejs-integration/package-lock.json generated Normal file
View File

@ -0,0 +1,598 @@
{
"name": "nodejs-integration",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodejs-integration",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@openfeature/server-sdk": "^1.18.0"
},
"devDependencies": {
"@types/node": "^20.19.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@openfeature/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.8.0.tgz",
"integrity": "sha512-FX/B6yMD2s4BlMKtB0PqSMl94eLaTwh0VK9URcMvjww0hqMOeGZnGv4uv9O5E58krAan7yCOCm4NBCoh2IATqw==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/@openfeature/server-sdk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@openfeature/server-sdk/-/server-sdk-1.18.0.tgz",
"integrity": "sha512-uP8nqEGBS58s3iWXx6d8vnJ6ZVt3DACJL4PWADOAuwIS4xXpID91783e9f6zQ0i1ijQpj3yx+3ZuCB2LfQMUMA==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@openfeature/core": "^1.7.0"
}
},
"node_modules/@types/node": {
"version": "20.19.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.19.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
"integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@ -0,0 +1,23 @@
{
"name": "nodejs-integration",
"version": "1.0.0",
"description": "Integration test for OpenFeature CLI Node.js generator",
"type": "module",
"scripts": {
"test": "tsx test.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@openfeature/server-sdk": "^1.18.0"
},
"devDependencies": {
"@types/node": "^20.19.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=22"
}
}

View File

@ -0,0 +1,99 @@
import { OpenFeature, JsonValue } from "@openfeature/server-sdk";
import { existsSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Simple test provider
class TestProvider {
get name() { return 'test-provider'; }
get metadata() { return { name: 'test-provider' }; }
async resolveBooleanEvaluation(flagKey: string, defaultValue: boolean) {
return { value: flagKey === 'enableFeatureA' ? true : defaultValue, reason: 'STATIC' };
}
async resolveStringEvaluation(flagKey: string, defaultValue: string) {
return { value: flagKey === 'greetingMessage' ? 'Hello from test!' : defaultValue, reason: 'STATIC' };
}
async resolveNumberEvaluation(flagKey: string, defaultValue: number) {
const values: Record<string, number> = { usernameMaxLength: 100, discountPercentage: 0.25 };
return { value: values[flagKey] ?? defaultValue, reason: 'STATIC' };
}
async resolveObjectEvaluation<T>(flagKey: string, defaultValue: JsonValue) {
const values: Record<string, JsonValue> = {
themeCustomization: {
primaryColor: '#ff0000',
secondaryColor: '#00ff00'
} as T
};
return { value: values[flagKey] ?? defaultValue, reason: 'STATIC' };
}
}
async function main() {
try {
console.log('🚀 Node.js OpenFeature Integration Test');
// 1. Check generated files
const generatedDir = join(__dirname, 'generated');
if (!existsSync(generatedDir)) {
throw new Error('Generated directory not found');
}
const files = readdirSync(generatedDir);
const clientFile = files.find(file => file.includes('openfeature'));
if (!clientFile) {
throw new Error('openfeature.ts not found');
}
console.log(`✅ Found: ${clientFile}`);
const clientPath = join(generatedDir, clientFile);
// 3. Setup OpenFeature provider and test
await OpenFeature.setProvider(new TestProvider());
const { getGeneratedClient } = await import(clientPath);
const client = getGeneratedClient();
console.log('🧪 Testing flags...');
// Test each flag
const tests = [
{ name: 'enableFeatureA', expected: 'boolean' },
{ name: 'greetingMessage', expected: 'string' },
{ name: 'usernameMaxLength', expected: 'number' },
{ name: 'discountPercentage', expected: 'number' },
{ name: 'themeCustomization', expected: 'object' }
];
for (const test of tests) {
if (client[test.name]) {
const result = await client[test.name]();
const type = typeof result;
if (type === test.expected) {
console.log(`${test.name}: ${result} | type: (${type})`);
} else {
throw new Error(`${test.name} returned ${type}, expected ${test.expected}`);
}
} else {
console.log(`⚠️ ${test.name} method not found`);
process.exit(1);
}
}
console.log('🎉 All tests passed!');
process.exit(0);
} catch (error) {
console.error('❌ Test failed:', error.message);
process.exit(1);
}
}
main();