Compare commits

...

48 Commits

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
OpenFeature Bot fa82a179e1
chore(main): release 0.3.2 (#86)
Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-04-02 14:27:57 +00:00
Michael Beemer 3fbd94726d
feat: consolidate logging and support debug flag (#92)
* feat: add nodejs generator

Signed-off-by: Michael Beemer <michael.beemer@dynatrace.com>

* feat: add a nodejs generator

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* improve doc diff output

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* check out expected sha

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* fix variable name

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* feat: consolidate logging and support debug flag

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

* regenerate docs

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>

---------

Signed-off-by: Michael Beemer <michael.beemer@dynatrace.com>
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-04-02 14:21:20 +00:00
Michael Beemer a40b6a4d31
feat: add nodejs generator (#91)
Signed-off-by: Michael Beemer <michael.beemer@dynatrace.com>
2025-04-02 14:52:57 +01:00
Michael Beemer 9244276fc4
docs: add install, quick start, commands, and more to readme (#90)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-28 15:27:22 -04:00
Michael Beemer afa46d00b3
feat: add script to install the latest binary (#85)
## This PR

- add a script to install the latest binary

### Notes

This is a modified version of the script used in
[Vacuum](https://github.com/daveshanley/vacuum/blob/main/bin/install.sh).
I've tested it locally on Ubuntu in WSL2 with an X86 cpu.

### Follow-up Tasks

Once we've verified it works, I'll update the readme to include an
install section.

---------

Signed-off-by: Michael Beemer <michael.beemer@dynatrace.com>
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Kris Coleman <kris.blacksuitmedia@gmail.com>
Co-authored-by: Kris Coleman <kriscodeman@gmail.com>
2025-03-28 17:06:44 +00:00
Kris Coleman 9102d1390a
feat(cli): add stability annotations to generated Markdown documentation (#88)
closes #87

## This PR

This update introduces the addition of stability information to the
generated markdown content for commands in our CLI. The changes include
a new function, `addStabilityToMarkdown`, which checks if a command has
a 'stability' annotation and adds it to the markdown content
accordingly. This function is then used in both `generate.go` and
`init.go`. Additionally, formatting adjustments were made in `root.go`
for better readability.

### Notes
- Some of the logic felt a bit astonishing, so I left a lot of comments.
If this isn't helpful, I'd be happy to remove them.
- I tried to approach this in a way where the addedStabilityInfo was
inherent and not imperative.
- There is still one imperative func that needs to be used in each
command, I would be open to suggestions to eliminate it.
- The logic for customizing the markdown should work for any command we
implement in the future.
- I was going to try and use go templating to achieve this, but ended up
going with a 'mutate after the fact' approach.
- This approach could be utilized in the future to customize the docs
further, but the logic to do so is very procedural. In the future, I
would like to think up a more declarative approach, I think this could
improve it so it's more easily extensible and less to maintain.

### Follow-up Tasks
- Possibly introduce a more declarative approach so we can extend
markdown customization more easily in the future, without having to
write additional logic to figure out where to place the custom content.

### How to test
- [ ] run `make generate-docs` and you should see the stability info
output in the docs. Change or add a new stability info to a command, try
it, and you should see the changes updated.

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
2025-03-27 19:20:36 +00:00
Kris Coleman 05e094db68
feat: add contributing guide and generator readme (#80)
## This PR

Added a new CONTRIBUTING.md file with detailed instructions on how to
contribute new generators to the project. This includes steps from
forking the repository, implementing the generator logic, writing tests,
registering the generator in CLI, updating documentation, and creating a
pull request.

Also added a README.md in the internal/generators directory explaining
how each generator works. It details about `language.go` and
`language.tmpl` files that are essential parts of each generator along
with an example workflow of how these components interact to generate
code based on OpenFeature flag manifest.

### Related Issues
closes #69

Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
2025-03-18 23:08:49 +00:00
OpenFeature Bot 0f4ba1f5a2
chore(main): release 0.3.1 (#84)
🤖 I have created a release *beep* *boop*
---


## [0.3.1](https://github.com/open-feature/cli/compare/v0.3.0...v0.3.1)
(2025-03-18)


### 🐛 Bug Fixes

* binary name referenced in the dockerfile
([0e28e8e](0e28e8ec3b))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-03-18 20:03:47 +00:00
Michael Beemer 0e28e8ec3b
fix: binary name referenced in the dockerfile
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-18 16:02:02 -04:00
OpenFeature Bot e988d75996
chore(main): release 0.3.0 (#83)
🤖 I have created a release *beep* *boop*
---


## [0.3.0](https://github.com/open-feature/cli/compare/v0.2.0...v0.3.0)
(2025-03-18)


### ⚠ BREAKING CHANGES

* change binary name
([#82](https://github.com/open-feature/cli/issues/82))

### 🧹 Chore

* change binary name
([#82](https://github.com/open-feature/cli/issues/82))
([fdfe561](fdfe561d49))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-03-18 17:56:23 +00:00
Michael Beemer fdfe561d49
chore!: change binary name (#82)
## This PR

- changes the binary name from `openfeature-cli` to `openfeature`

### Notes

I'm working on an install script and noticed that the binary is
needlessly long.

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-18 15:55:09 +00:00
OpenFeature Bot 67f45c1e28
chore(main): release 0.2.0 (#77)
🤖 I have created a release *beep* *boop*
---


## [0.2.0](https://github.com/open-feature/cli/compare/v0.1.10...v0.2.0)
(2025-03-18)


### ⚠ BREAKING CHANGES

* add init command, update cli flags, support a config file
([#71](https://github.com/open-feature/cli/issues/71))

### 🧹 Chore

* rename the checksum file
([34afca6](34afca62ab))
* upgrade viper to 1.20
([#78](https://github.com/open-feature/cli/issues/78))
([6c36ee9](6c36ee90f7))


### 🔄 Refactoring

* add init command, update cli flags, support a config file
([#71](https://github.com/open-feature/cli/issues/71))
([106bf9d](106bf9ddfe))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>
2025-03-18 13:07:17 +00:00
Michael Beemer 34afca62ab
chore: rename the checksum file
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-18 08:58:23 -04:00
Michael Beemer 6c36ee90f7
chore: upgrade viper to 1.20 (#78)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-16 11:51:46 -04:00
Michael Beemer 106bf9ddfe
refactor!: add init command, update cli flags, support a config file (#71)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
2025-03-14 16:12:26 -04:00
127 changed files with 9639 additions and 1251 deletions

View File

@ -4,6 +4,7 @@ on:
branches:
- main
pull_request:
merge_group:
permissions:
# Required: allow read access to the content for analysis.
@ -27,5 +28,5 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
only-new-issues: true
version: v1.64
only-new-issues: true

View File

@ -31,6 +31,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
@ -38,4 +39,27 @@ jobs:
- run: make generate-docs
- name: Check no diff
run: |
if [ ! -z "$(git status --porcelain)" ]; then echo "::error file=Makefile::Doc generation produced diff. Run 'make generate-docs' and commit results."; exit 1; fi
if [ ! -z "$(git status --porcelain)" ]; then
echo "::error file=Makefile::Doc generation produced diff. Run 'make generate-docs' and commit results."
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'

10
.gitignore vendored
View File

@ -24,3 +24,13 @@ go.work.sum
# env file
.env
dist
# openfeature cli config
.openfeature.yaml
.idea/
node_modules/
npm-debug.log*
generated/
*.log

View File

@ -7,7 +7,7 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
project_name: openfeature-cli
project_name: openfeature
before:
hooks:
@ -21,9 +21,10 @@ builds:
- linux
- windows
- darwin
binary: ./cmd/openfeature
archives:
- format: tar.gz
- formats: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
@ -35,46 +36,49 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
formats: ["zip"]
checksum:
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 }}
- --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 }}
- --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.1.10"
".": "0.3.5"
}

View File

@ -1,5 +1,91 @@
# 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)
### ✨ New Features
* add contributing guide and generator readme ([#80](https://github.com/open-feature/cli/issues/80)) ([05e094d](https://github.com/open-feature/cli/commit/05e094db68c210271205f6a043fc885d1a3c23b8)), closes [#69](https://github.com/open-feature/cli/issues/69)
* add nodejs generator ([#91](https://github.com/open-feature/cli/issues/91)) ([a40b6a4](https://github.com/open-feature/cli/commit/a40b6a4d31d6f290ccdd9475bedbbe947aad510e))
* add script to install the latest binary ([#85](https://github.com/open-feature/cli/issues/85)) ([afa46d0](https://github.com/open-feature/cli/commit/afa46d00b303de8bf34197369fe34fd6022c34b9))
* **cli:** add stability annotations to generated Markdown documentation ([#88](https://github.com/open-feature/cli/issues/88)) ([9102d13](https://github.com/open-feature/cli/commit/9102d1390ace7e3b285ae4ce38208b229de59cbf))
* consolidate logging and support debug flag ([#92](https://github.com/open-feature/cli/issues/92)) ([3fbd947](https://github.com/open-feature/cli/commit/3fbd94726d581f1911ad8e539b004dd843503ef4))
### 📚 Documentation
* add install, quick start, commands, and more to readme ([#90](https://github.com/open-feature/cli/issues/90)) ([9244276](https://github.com/open-feature/cli/commit/9244276fc47128a7a304ef22732ad5dcde38c3e8))
## [0.3.1](https://github.com/open-feature/cli/compare/v0.3.0...v0.3.1) (2025-03-18)
### 🐛 Bug Fixes
* binary name referenced in the dockerfile ([0e28e8e](https://github.com/open-feature/cli/commit/0e28e8ec3b4108eee6ae43f587201ff7cbf18020))
## [0.3.0](https://github.com/open-feature/cli/compare/v0.2.0...v0.3.0) (2025-03-18)
### ⚠ BREAKING CHANGES
* change binary name ([#82](https://github.com/open-feature/cli/issues/82))
### 🧹 Chore
* change binary name ([#82](https://github.com/open-feature/cli/issues/82)) ([fdfe561](https://github.com/open-feature/cli/commit/fdfe561d49e17017af5165dfe0eec359387935e4))
## [0.2.0](https://github.com/open-feature/cli/compare/v0.1.10...v0.2.0) (2025-03-18)
### ⚠ BREAKING CHANGES
* add init command, update cli flags, support a config file ([#71](https://github.com/open-feature/cli/issues/71))
### 🧹 Chore
* rename the checksum file ([34afca6](https://github.com/open-feature/cli/commit/34afca62ab6cf229f38b0cc81d6f6443cf1ac8ea))
* upgrade viper to 1.20 ([#78](https://github.com/open-feature/cli/issues/78)) ([6c36ee9](https://github.com/open-feature/cli/commit/6c36ee90f796cdabe318ef59aec9de3d93c3ffd5))
### 🔄 Refactoring
* add init command, update cli flags, support a config file ([#71](https://github.com/open-feature/cli/issues/71)) ([106bf9d](https://github.com/open-feature/cli/commit/106bf9ddfe93673d956487bcf84667d550543aa0))
## [0.1.10](https://github.com/open-feature/cli/compare/v0.1.9...v0.1.10) (2025-01-27)

207
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,207 @@
# 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:
1. **Fork the Repository**: Start by forking the repository to your GitHub account.
2. **Clone the Repository**: Clone the forked repository to your local machine.
3. **Create a New Branch**: Create a new branch for your generator. Use a descriptive name for the branch, such as `feature/add-new-generator`.
4. **Add Your Generator**: Add your generator in the appropriate directory under `/internal/generate/generators/`. For example, if you are adding a generator for Python, you might create a new directory `/internal/generate/generators/python/` and add your files there.
5. **Implement the Generator**: Implement the generator logic. Ensure that your generator follows the existing patterns and conventions used in the project. Refer to the existing generators like `/internal/generate/generators/golang` or `/internal/generate/generators/react` for examples.
6. **Write Tests**: Write tests for your generator to ensure it works as expected. Add your tests in the appropriate test directory, such as `/internal/generate/generators/python/`. Write tests for any commands you may add, too. Add your command tests in the appropriate test directory, such as `cmd/generate_test.go`.
7. **Register the Generator**: After implementing your generator, you need to register it in the CLI under the `generate` command. Follow these steps to register your generator:
- **Create a New Command Directory**: Create a new directory under `cmd/generate` with the name of your target language. For example, if you are adding a generator for Python, create a new directory `cmd/generate/python/`.
- **Add Command File**: In the new directory, create a file named `python.go` (replace `python` with the name of your target language). This file will define the CLI command for your generator.
- **Implement Command**: Implement the command logic in the `python.go` file. Refer to the existing commands like `cmd/generate/golang/golang.go` or `cmd/generate/react/react.go` for examples.
- **Register Command**: Open the `cmd/generate/generate.go` file and register your new command as a subcommand. Add an import statement for your new command package and call `Root.AddCommand(python.Cmd)` (replace `python` with the name of your target language).
8. **Update Documentation**: Update the documentation to include information about your new generator. This may include updating the README.md and any other relevant documentation files. You can run `make generate-docs` to assist with documentation updates.
9. **Commit and Push**: Commit your changes and push the new branch to your forked repository.
10. **Create a Pull Request**: Create a pull request from your new branch to the main repository. Provide a clear and detailed description of your changes, including the purpose of the new generator and any relevant information.
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
The `TemplateData` struct is used to pass data to the templates.
### Built-in template functions
The following functions are automatically included in the templates:
#### ToPascal
Converts a string to `PascalCase`
```go
{{ "hello world" | ToPascal }} // HelloWorld
```
#### ToCamel
Converts a string to `camelCase`
```go
{{ "hello world" | ToCamel }} // helloWorld
```
#### ToKebab
Converts a string to `kebab-case`
```go
{{ "hello world" | ToKebab }} // hello-world
```
#### ToSnake
Converts a string to `snake_case`
```go
{{ "hello world" | ToSnake }} // hello_world
```
#### ToScreamingSnake
Converts a string to `SCREAMING_SNAKE_CASE`
```go
{{ "hello world" | ToScreamingSnake }} // HELLO_WORLD
```
#### ToUpper
Converts a string to `UPPER CASE`
```go
{{ "hello world" | ToUpper }} // HELLO WORLD
```
#### ToLower
Converts a string to `lower case`
```go
{{ "HELLO WORLD" | ToLower }} // hello world
```
#### ToTitle
Converts a string to `Title Case`
```go
{{ "hello world" | ToTitle }} // Hello World
```
#### Quote
Wraps a string in double quotes
```go
{{ "hello world" | Quote }} // "hello world"
```
#### QuoteString
Wraps only strings in double quotes
```go
{{ "hello world" | QuoteString }} // "hello world"
{{ 123 | QuoteString }} // 123
```
### Custom template functions
You can add custom template functions by passing a `FuncMap` to the `GenerateFile` function.

30
DESIGN.md Normal file
View File

@ -0,0 +1,30 @@
# OpenFeature CLI Design
This document describes the design considerations and goals for the OpenFeature CLI tool.
## Why Code Generation?
Code generation automates the creation of strongly typed flag accessors, minimizing configuration errors and providing a better developer experience.
By generating these accessors, developers can avoid issues related to incorrect flag names or types, resulting in more reliable and maintainable code.
Benefits of the code generation approach:
- **Type Safety**: Catch flag-related errors at compile time instead of runtime
- **IDE Support**: Get autocomplete and documentation for your flags
- **Refactoring Support**: Rename flags and the changes propagate throughout your codebase
- **Discoverability**: Make it easier for developers to find and use available flags
## Goals
- **Unified Flag Manifest Format**: Establish a standardized flag manifest format that can be easily converted from existing configurations.
- **Strongly Typed Flag Accessors**: Develop a CLI tool to generate strongly typed flag accessors for multiple programming languages.
- **Modular and Extensible Design**: Create a format that allows for future extensions and modularization of flags.
- **Language Agnostic**: Support multiple programming languages through a common flag manifest format.
- **Provider Independence**: Work with any feature flag provider that can be adapted to the OpenFeature API.
## Non-Goals
- **Full Provider Integration**: The initial scope does not include creating tools to convert provider-specific configurations to the new flag manifest format.
- **Validation of Flag Configs**: The project will not initially focus on validating flag configurations for consistency with the flag manifest.
- **General-Purpose Configuration**: The project will not aim to create a general-purpose configuration tool for feature flags beyond the scope of the code generation tool.
- **Runtime Flag Management**: The CLI is not intended to replace provider SDKs for runtime flag evaluation.

View File

@ -1,7 +1,7 @@
FROM alpine:3.21
FROM alpine:3.22
COPY ./openfeature-cli usr/local/bin/openfeature-cli
COPY ./openfeature usr/local/bin/openfeature
RUN chmod +x /usr/local/bin/openfeature-cli
RUN chmod +x /usr/local/bin/openfeature
ENTRYPOINT ["/usr/local/bin/openfeature-cli"]
ENTRYPOINT ["/usr/local/bin/openfeature"]

View File

@ -1,11 +1,42 @@
.PHONY: test
test:
@echo "Running tests..."
go test -v ./...
@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
@echo "Documentation generated successfully!"
@go run ./docs/generate-commands.go
@echo "Documentation generated successfully!"
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!"

216
README.md
View File

@ -12,7 +12,7 @@
<!-- The 'github-badges' class is used in the docs -->
<p align="center" class="github-badges">
<a href="https://github.com/orgs/open-feature/projects/17">
<img alt="work-in-progress" src="https://img.shields.io/badge/status-WIP-red" />
<img alt="work-in-progress" src="https://img.shields.io/badge/status-WIP-yellow" />
</a>
<a href="https://cloud-native.slack.com/archives/C07DY4TUDK6">
<img alt="Slack" src="https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack" />
@ -30,42 +30,216 @@
## Overview
The OpenFeature CLI is a command-line tool designed to improve the developer experience when working with feature flags.
Currently, features are focused primarily on supporting code generation.
It helps developers manage feature flags consistently across different environments and programming languages by providing powerful utilities for code generation, flag validation, and more.
## Installation
Download packaged binaries from the [releases page](https://github.com/open-feature/codegen/releases).
### via curl
### Why Code Generation?
The OpenFeature CLI can be installed using a shell command.
This method is suitable for most Unix-like operating systems.
Code generation automates the creation of strongly typed flag accessors, minimizing configuration errors and providing a better developer experience.
By generating these accessors, developers can avoid issues related to incorrect flag names or types, resulting in more reliable and maintainable code.
```bash
curl -fsSL https://openfeature.dev/scripts/install_cli.sh | sh
```
### Goals
### via Docker
- **Unified Flag Manifest Format**: Establish a standardized flag manifest format that can be easily converted from existing configurations.
- **Strongly Typed Flag Accessors**: Develop a CLI tool to generate strongly typed flag accessors for multiple programming languages.
- **Modular and Extensible Design**: Create a format that allows for future extensions and modularization of flags.
The OpenFeature CLI is available as a Docker image in the [GitHub Container Registry](https://github.com/open-feature/cli/pkgs/container/cli).
### Non-Goals
You can run the CLI in a Docker container using the following command:
- **Full Provider Integration**: The initial scope does not include creating tools to convert provider-specific configurations to the new flag manifest format.
- **Validation of Flag Configs**: The project will not initially focus on validating flag configurations for consistency with the flag manifest.
- **General-Purpose Configuration**: The project will not aim to create a general-purpose configuration tool for feature flags beyond the scope of the code generation tool.
```bash
docker run -it -v $(pwd):/local -w /local ghcr.io/open-feature/cli:latest
```
## Support the project
### via Go
- Give this repo a ⭐️!
- Follow us on social media:
If you have `Go >= 1.23` installed, you can install the CLI using the following command:
```bash
go install github.com/open-feature/cli/cmd/openfeature@latest
```
### via pre-built binaries
Download the appropriate pre-built binary from the [releases page](https://github.com/open-feature/cli/releases).
## Quick Start
1. Create a flag manifest file in your project root:
```bash
cat > flags.json << EOL
{
"$schema": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag-manifest.json",
"flags": {
"enableMagicButton": {
"flagType": "boolean",
"defaultValue": false,
"description": "Activates a special button that enhances user interaction with magical, intuitive functionalities."
}
}
}
EOL
```
> [!NOTE]
> This is for demonstration purposes only.
> In a real-world scenario, you would typically want to fetch this file from a remote flag management service.
> See [here](https://github.com/open-feature/cli/issues/3), more more details.
2. Generate code for your preferred language:
```bash
openfeature generate react
```
See [here](./docs/commands/openfeature_generate.md) for all available options.
3. View the generated code:
```bash
cat openfeature.ts
```
**Congratulations!**
You have successfully generated your first strongly typed flag accessors.
You can now use the generated code in your application to access the feature flags.
This is just scratching the surface of what the OpenFeature CLI can do.
For more advanced usage, read on!
## Commands
The OpenFeature CLI provides the following commands:
### `init`
Initialize a new flag manifest in your project.
```bash
openfeature init
```
See [here](./docs/commands/openfeature_init.md), for all available options.
### `generate`
Generate strongly typed flag accessors for your project.
```bash
# Available languages
openfeature generate
# Basic usage
openfeature generate [language]
# With custom output directory
openfeature generate typescript --output ./src/flags
```
See [here](./docs/commands/openfeature_generate.md), for all available options.
### `version`
Print the version number of the OpenFeature CLI.
```bash
openfeature version
```
See [here](./docs/commands/openfeature_version.md), for all available options.
## Flag Manifest
The flag manifest is a JSON file that defines your feature flags and their properties.
It serves as the source of truth for your feature flags and is used by the CLI to generate strongly typed accessors.
The manifest file should be named `flags.json` and placed in the root of your project.
### Flag Manifest Structure
The flag manifest file should follow the JSON schema defined [here](https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag-manifest.json).
The schema defines the following properties:
- `$schema`: The URL of the JSON schema for validation.
- `flags`: An object containing the feature flags.
- `flagKey`: A unique key for the flag.
- `description`: A description of what the flag does.
- `type`: The type of the flag (e.g., `boolean`, `string`, `number`, `object`).
- `defaultValue`: The default value of the flag.
### Example Flag Manifest
```json
{
"$schema": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag-manifest.json",
"flags": {
"uniqueFlagKey": {
"description": "Description of what this flag does",
"type": "boolean|string|number|object",
"defaultValue": "default-value",
}
}
}
```
## Configuration
The OpenFeature CLI uses an optional configuration file to override default settings and customize the behavior of the CLI.
This file can be in JSON or YAML format and should be named either `.openfeature.json` or `.openfeature.yaml`.
### Configuration File Structure
```yaml
# Example .openfeature.yaml
manifest: "flags/manifest.json" # Overrides the default manifest path
generate:
output: "src/flags" # Overrides the default output directory
# Any language-specific options can be specified here
# For example, for React:
react:
output: "src/flags/react" # Overrides the default React output directory
# For Go:
go:
package: "github.com/myorg/myrepo/flags" # Overrides the default Go package name
output: "src/flags/go" # Overrides the default Go output directory
```
### Configuration Priority
The CLI uses a layered approach to configuration, allowing you to override settings at different levels.
The configuration is applied in the following order:
```mermaid
flowchart LR
default("Default Config")
config("Config File")
args("Command Line Args")
default --> config
config --> args
```
### Get Involved
- **CNCF Slack**: Join the conversation in the [#openfeature](https://cloud-native.slack.com/archives/C0344AANLA1) and [#openfeature-cli](https://cloud-native.slack.com/archives/C07DY4TUDK6) channel
- **Regular Meetings**: Attend our [community calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/openfeature)
- **GitHub Issues**: Report bugs or request features in our [issue tracker](https://github.com/open-feature/cli/issues)
- **Social Media**:
- Twitter: [@openfeature](https://twitter.com/openfeature)
- LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
- For more, check out our [community page](https://openfeature.dev/community/)
For more information, visit our [community page](https://openfeature.dev/community/).
### Support the project
- Give this repo a ⭐️!
- Share your experience and contribute back to the project
### Thanks to everyone who has already contributed
<a href="https://github.com/open-feature/codegen/graphs/contributors">
<img src="https://contrib.rocks/image?repo=open-feature/codegen" alt="Pictures of the folks who have contributed to the project" />
<a href="https://github.com/open-feature/cli/graphs/contributors">
<img src="https://contrib.rocks/image?repo=open-feature/cli" alt="Pictures of the folks who have contributed to the project" />
</a>
Made with [contrib.rocks](https://contrib.rocks).

262
bin/install.sh Executable file
View File

@ -0,0 +1,262 @@
#!/bin/sh
set -e
# Adapted/Copied from https://github.com/daveshanley/vacuum/blob/main/bin/install.sh
if [ -d "$HOME/.local/bin" ] || mkdir -p "$HOME/.local/bin" 2>/dev/null; then
DEFAULT_INSTALL_DIR="$HOME/.local/bin"
elif [ -w "/usr/local/bin" ]; then
DEFAULT_INSTALL_DIR="/usr/local/bin"
else
fmt_error "unable to write to $HOME/.local/bin or /usr/local/bin"
fmt_error "Please run this script with sudo or set INSTALL_DIR to a directory you can write to."
exit 1
fi
INSTALL_DIR=${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}
BINARY_NAME=${BINARY_NAME:-"openfeature"}
REPO_NAME="open-feature/cli"
ISSUE_URL="https://github.com/open-feature/cli/issues/new"
# get_latest_release "open-feature/cli"
get_latest_release() {
curl --retry 5 --silent "https://api.github.com/repos/$1/releases/latest" | # Get latest release from GitHub api
grep '"tag_name":' | # Get tag line
sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value
}
get_asset_name() {
echo "openfeature_$1_$2.tar.gz"
}
get_download_url() {
local asset_name=$(get_asset_name $2 $3)
echo "https://github.com/open-feature/cli/releases/download/v$1/${asset_name}"
}
get_checksum_url() {
echo "https://github.com/open-feature/cli/releases/download/v$1/checksums.txt"
}
command_exists() {
command -v "$@" >/dev/null 2>&1
}
fmt_error() {
echo ${RED}"Error: $@"${RESET} >&2
}
fmt_warning() {
echo ${YELLOW}"Warning: $@"${RESET} >&2
}
fmt_underline() {
echo "$(printf '\033[4m')$@$(printf '\033[24m')"
}
fmt_code() {
echo "\`$(printf '\033[38;5;247m')$@${RESET}\`"
}
setup_color() {
# Only use colors if connected to a terminal
if [ -t 1 ]; then
RED=$(printf '\033[31m')
GREEN=$(printf '\033[32m')
YELLOW=$(printf '\033[33m')
BLUE=$(printf '\033[34m')
MAGENTA=$(printf '\033[35m')
BOLD=$(printf '\033[1m')
RESET=$(printf '\033[m')
else
RED=""
GREEN=""
YELLOW=""
BLUE=""
MAGENTA=""
BOLD=""
RESET=""
fi
}
get_os() {
case "$(uname -s)" in
*linux* ) echo "Linux" ;;
*Linux* ) echo "Linux" ;;
*darwin* ) echo "Darwin" ;;
*Darwin* ) echo "Darwin" ;;
esac
}
get_machine() {
case "$(uname -m)" in
"x86_64"|"amd64"|"x64")
echo "x86_64" ;;
"i386"|"i86pc"|"x86"|"i686")
echo "i386" ;;
"arm64"|"armv6l"|"aarch64")
echo "arm64"
esac
}
get_tmp_dir() {
echo $(mktemp -d)
}
do_checksum() {
checksum_url=$(get_checksum_url $version)
get_checksum_url $version
expected_checksum=$(curl -sL $checksum_url | grep $asset_name | awk '{print $1}')
if command_exists sha256sum; then
checksum=$(sha256sum $asset_name | awk '{print $1}')
elif command_exists shasum; then
checksum=$(shasum -a 256 $asset_name | awk '{print $1}')
else
fmt_warning "Could not find a checksum program. Install shasum or sha256sum to validate checksum."
return 0
fi
if [ "$checksum" != "$expected_checksum" ]; then
fmt_error "Checksums do not match"
exit 1
fi
}
do_install_binary() {
asset_name=$(get_asset_name $os $machine)
download_url=$(get_download_url $version $os $machine)
command_exists curl || {
fmt_error "curl is not installed"
exit 1
}
command_exists tar || {
fmt_error "tar is not installed"
exit 1
}
local tmp_dir=$(get_tmp_dir)
# Download tar.gz to tmp directory
echo "Downloading $download_url"
(cd $tmp_dir && curl -sL -O "$download_url")
(cd $tmp_dir && do_checksum)
# Extract download
(cd $tmp_dir && tar -xzf "$asset_name")
# Install binary
if [ -w "$INSTALL_DIR" ]; then
mv "$tmp_dir/$BINARY_NAME" "$INSTALL_DIR"
else
fmt_error "Unable to write to $INSTALL_DIR. Please run this script with sudo or set INSTALL_DIR to a directory you can write to."
exit 1
fi
# Make the binary executable
if [ -w "$INSTALL_DIR/$BINARY_NAME" ]; then
chmod +x "$INSTALL_DIR/$BINARY_NAME"
else
sudo chmod +x "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || {
fmt_error "Could not make $INSTALL_DIR/$BINARY_NAME executable"
exit 1
}
fi
# Check if the binary is executable
if [ ! -x "$INSTALL_DIR/$BINARY_NAME" ]; then
fmt_error "The binary is not executable. Please check your permissions."
exit 1
fi
echo "Installed the OpenFeature cli to $INSTALL_DIR"
# Add to PATH information if not already in PATH
if ! echo "$PATH" | tr ":" "\n" | grep -q "^$INSTALL_DIR$"; then
shell_profile=""
case $SHELL in
*/bash*)
if [ -f "$HOME/.bashrc" ]; then
shell_profile="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
shell_profile="$HOME/.bash_profile"
fi
;;
*/zsh*)
shell_profile="$HOME/.zshrc"
;;
esac
if [ -n "$shell_profile" ]; then
echo ""
echo "${YELLOW}$INSTALL_DIR is not in your PATH.${RESET}"
echo "To add it to your PATH, run:"
echo " echo 'export PATH=\"\$PATH:$INSTALL_DIR\"' >> $shell_profile"
echo "Then, restart your terminal or run:"
echo " source $shell_profile"
fi
fi
# Cleanup
rm -rf "$tmp_dir"
}
install_termux() {
echo "Installing the OpenFeature cli, this may take a few minutes..."
pkg upgrade && pkg install golang git -y && git clone https://github.com/open-feature/cli.git && cd cli/ && go build -o $PREFIX/bin/openfeature
}
main() {
setup_color
latest_tag=$(get_latest_release $REPO_NAME)
latest_version=$(echo $latest_tag | sed 's/v//')
version=${VERSION:-$latest_version}
os=$(get_os)
if test -z "$os"; then
fmt_error "$(uname -s) os type is not supported"
echo "Please create an issue so we can add support. $ISSUE_URL"
exit 1
fi
machine=$(get_machine)
if test -z "$machine"; then
fmt_error "$(uname -m) machine type is not supported"
echo "Please create an issue so we can add support. $ISSUE_URL"
exit 1
fi
if [ ${TERMUX_VERSION} ] ; then
install_termux
else
echo "Installing OpenFeature CLI to $INSTALL_DIR..."
echo "To use a different install location, press Ctrl+C and run again with:"
echo " INSTALL_DIR=/your/custom/path ./bin/install.sh"
echo ""
sleep 2 # Give user a chance to cancel if needed
do_install_binary
fi
printf "$MAGENTA"
cat <<'EOF'
___ _____ _
/ _ \ _ __ ___ _ __ | ___|__ __ _| |_ _ _ _ __ ___
| | | | '_ \ / _ \ '_ \| |_ / _ \/ _` | __| | | | '__/ _ \
| |_| | |_) | __/ | | | _| __/ (_| | |_| |_| | | | __/
\___/| .__/ \___|_| |_|_| \___|\__,_|\__|\__,_|_| \___|
|_|
CLI
Run `openfeature help` for commands
EOF
printf "$RESET"
}
main

View File

@ -1,23 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra/doc"
)
// GenerateDoc generates cobra docs of the cmd
func GenerateDoc(path string) error {
linkHandler := func(name string) string {
return name
}
filePrepender := func(filename string) string {
return "<!-- markdownlint-disable-file -->\n<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->\n"
}
if err := doc.GenMarkdownTreeCustom(rootCmd, path, filePrepender, linkHandler); err != nil {
return fmt.Errorf("error generating docs: %w", err)
}
return nil
}

View File

@ -1,31 +0,0 @@
package generate
import (
"github.com/open-feature/cli/cmd/generate/golang"
"github.com/open-feature/cli/cmd/generate/react"
"github.com/open-feature/cli/internal/flagkeys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Root for `generate“ sub-commands, handling code generation for flag accessors.
var Root = &cobra.Command{
Use: "generate",
Short: "Code generation for flag accessors for OpenFeature.",
Long: `Code generation for flag accessors for OpenFeature.`,
}
func init() {
// Add subcommands.
Root.AddCommand(golang.Cmd)
Root.AddCommand(react.Cmd)
// Add flags.
Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.")
Root.MarkPersistentFlagRequired(flagkeys.FlagManifestPath)
viper.BindPFlag(flagkeys.FlagManifestPath, Root.PersistentFlags().Lookup(flagkeys.FlagManifestPath))
Root.PersistentFlags().String(flagkeys.OutputPath, "", "Output path for the codegen")
viper.BindPFlag(flagkeys.OutputPath, Root.PersistentFlags().Lookup(flagkeys.OutputPath))
Root.MarkPersistentFlagRequired(flagkeys.OutputPath)
}

View File

@ -1,103 +0,0 @@
package generate
import (
"os"
"path/filepath"
"testing"
"github.com/open-feature/cli/internal/flagkeys"
"github.com/google/go-cmp/cmp"
"github.com/spf13/afero"
"github.com/spf13/viper"
)
func TestGenerateGoSuccess(t *testing.T) {
// Constant paths.
const memoryManifestPath = "manifest/path.json"
const memoryOutputPath = "output/path.go"
const packageName = "testpackage"
const testFileManifest = "testdata/success_manifest.golden"
const testFileGo = "testdata/success_go.golden"
// Prepare in-memory files.
fs := afero.NewMemMapFs()
viper.Set(flagkeys.FileSystem, fs)
readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs)
// Prepare command.
Root.SetArgs([]string{"go",
"--flag_manifest_path", memoryManifestPath,
"--output_path", memoryOutputPath,
"--package_name", packageName,
})
// Run command.
Root.Execute()
// Compare result.
compareOutput(t, testFileGo, memoryOutputPath, fs)
}
func TestGenerateReactSuccess(t *testing.T) {
// Constant paths.
const memoryManifestPath = "manifest/path.json"
const memoryOutputPath = "output/path.ts"
const testFileManifest = "testdata/success_manifest.golden"
const testFileReact = "testdata/success_react.golden"
// Prepare in-memory files.
fs := afero.NewMemMapFs()
viper.Set(flagkeys.FileSystem, fs)
readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs)
// Prepare command.
Root.SetArgs([]string{"react",
"--flag_manifest_path", memoryManifestPath,
"--output_path", memoryOutputPath,
})
// Run command.
Root.Execute()
// Compare result.
compareOutput(t, testFileReact, memoryOutputPath, fs)
}
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
data, err := os.ReadFile(inputPath)
if err != nil {
t.Fatalf("error reading file %q: %v", inputPath, err)
}
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err)
}
f, err := memFs.Create(memPath)
if err != nil {
t.Fatalf("error creating file %q: %v", memPath, err)
}
defer f.Close()
writtenBytes, err := f.Write(data)
if err != nil {
t.Fatalf("error writing contents to file %q: %v", memPath, err)
}
if writtenBytes != len(data) {
t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath)
}
}
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
want, err := os.ReadFile(testFile)
if err != nil {
t.Fatalf("error reading file %q: %v", testFile, err)
}
got, err := afero.ReadFile(fs, memoryOutputPath)
if err != nil {
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("output mismatch (-want +got):\n%s", diff)
}
}

View File

@ -1,32 +0,0 @@
package golang
import (
"github.com/open-feature/cli/internal/flagkeys"
"github.com/open-feature/cli/internal/generate"
"github.com/open-feature/cli/internal/generate/plugins/golang"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Cmd for `generate“ command, handling code generation for flag accessors
var Cmd = &cobra.Command{
Use: "go",
Short: "Generate Golang flag accessors for OpenFeature.",
Long: `Generate Golang flag accessors for OpenFeature.`,
RunE: func(cmd *cobra.Command, args []string) error {
params := golang.Params{
GoPackage: viper.GetString(flagkeys.GoPackageName),
}
gen := golang.NewGenerator(params)
err := generate.CreateFlagAccessors(gen)
return err
},
}
func init() {
Cmd.Flags().String(flagkeys.GoPackageName, "", "Name of the Go package to be generated.")
Cmd.MarkFlagRequired(flagkeys.GoPackageName)
viper.BindPFlag(flagkeys.GoPackageName, Cmd.Flags().Lookup(flagkeys.GoPackageName))
}

View File

@ -1,24 +0,0 @@
package react
import (
"github.com/open-feature/cli/internal/generate"
"github.com/open-feature/cli/internal/generate/plugins/react"
"github.com/spf13/cobra"
)
// Cmd for "generate" command, handling code generation for flag accessors
var Cmd = &cobra.Command{
Use: "react",
Short: "Generate typesafe React Hooks.",
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
RunE: func(cmd *cobra.Command, args []string) error {
params := react.Params{}
gen := react.NewGenerator(params)
err := generate.CreateFlagAccessors(gen)
return err
},
}
func init() {
}

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

@ -1,41 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/open-feature/cli/cmd/generate"
"github.com/spf13/cobra"
)
var (
Version = "dev"
Commit string
Date string
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "openfeature",
Short: "CLI for OpenFeature.",
Long: `CLI for OpenFeature related functionalities.`,
DisableAutoGenTag: true,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(version string, commit string, date string) {
Version = version
Commit = commit
Date = date
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
rootCmd.AddCommand(generate.Root)
rootCmd.AddCommand(versionCmd)
}

View File

@ -1,31 +0,0 @@
package cmd
import (
"fmt"
"runtime/debug"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of the OpenFeature CLI",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
if Version == "dev" {
details, ok := debug.ReadBuildInfo()
if ok && details.Main.Version != "" && details.Main.Version != "(devel)" {
Version = details.Main.Version
for _, i := range details.Settings {
if i.Key == "vcs.time" {
Date = i.Value
}
if i.Key == "vcs.revision" {
Commit = i.Value
}
}
}
}
fmt.Printf("OpenFeature CLI: %s (%s), built at: %s\n", Version, Commit, Date)
},
}

View File

@ -8,14 +8,23 @@ CLI for OpenFeature.
CLI for OpenFeature related functionalities.
```
openfeature [flags]
```
### Options
```
-h, --help help for openfeature
--debug Enable debug logging
-h, --help help for openfeature
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature.
* [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

@ -2,23 +2,35 @@
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate
Code generation for flag accessors for OpenFeature.
Generate typesafe OpenFeature accessors.
### Synopsis
Code generation for flag accessors for OpenFeature.
```
openfeature generate [flags]
```
### Options
```
--flag_manifest_path string Path to the flag manifest.
-h, --help help for generate
--output_path string Output path for the codegen
-h, --help help for generate
-o, --output string Path to where the generated files should be saved
```
### 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.
* [openfeature generate go](openfeature_generate_go.md) - Generate Golang flag accessors 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

@ -2,11 +2,14 @@
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate go
Generate Golang flag accessors for OpenFeature.
Generate typesafe accessors for OpenFeature.
> **Stability**: alpha
### Synopsis
Generate Golang flag accessors for OpenFeature.
Generate typesafe accessors compatible with the OpenFeature Go SDK.
```
openfeature generate go [flags]
@ -16,17 +19,19 @@ openfeature generate go [flags]
```
-h, --help help for go
--package_name string Name of the Go package to be generated.
--package-name string Name of the generated Go package (default "openfeature")
```
### Options inherited from parent commands
```
--flag_manifest_path string Path to the flag manifest.
--output_path string Output path for the codegen
--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) - Code generation for flag accessors for OpenFeature.
* [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 nodejs
Generate typesafe Node.js client.
> **Stability**: alpha
### Synopsis
Generate typesafe Node.js client compatible with the OpenFeature JavaScript Server SDK.
```
openfeature generate nodejs [flags]
```
### Options
```
-h, --help help for nodejs
```
### 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

@ -4,6 +4,9 @@
Generate typesafe React Hooks.
> **Stability**: alpha
### Synopsis
Generate typesafe React Hooks compatible with the OpenFeature React SDK.
@ -21,11 +24,13 @@ openfeature generate react [flags]
### Options inherited from parent commands
```
--flag_manifest_path string Path to the flag manifest.
--output_path string Output path for the codegen
--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) - Code generation for flag accessors for OpenFeature.
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -0,0 +1,33 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature init
Initialize a new project
### Synopsis
Initialize a new project for OpenFeature CLI.
```
openfeature init [flags]
```
### Options
```
-h, --help help for init
--override Override an existing configuration
```
### 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

@ -14,6 +14,14 @@ openfeature version [flags]
-h, --help help for version
```
### 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

@ -1,16 +1,144 @@
package main
import (
"log"
"fmt"
"os"
"regexp"
"strings"
"github.com/open-feature/cli/cmd"
"github.com/open-feature/cli/internal/cmd"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
const docPath = "./docs/commands"
// GenerateDoc generates cobra docs of the cmd
// addStabilityToMarkdown adds stability information to the generated markdown content
func addStabilityToMarkdown(cmd *cobra.Command, content string) string {
if stability, ok := cmd.Annotations["stability"]; ok {
// Look for existing stability info to replace
oldStabilityPattern := "\n> \\*\\*Stability\\*\\*: [a-z]+\n\n"
hasExistingStability := regexp.MustCompile(oldStabilityPattern).MatchString(content)
if hasExistingStability {
// Replace existing stability info with the current one
return regexp.MustCompile(oldStabilityPattern).ReplaceAllString(
content,
fmt.Sprintf("\n> **Stability**: %s\n\n", stability),
)
}
// If no existing stability info, insert it
// Look for the pattern of command title, description, and then either ### Synopsis or ```
cmdNameLine := fmt.Sprintf("## openfeature%s", cmd.CommandPath()[11:])
cmdNameIndex := strings.Index(content, cmdNameLine)
if cmdNameIndex != -1 {
// Find the end of the description section
var insertPoint int
synopsisIndex := strings.Index(content, "### Synopsis")
codeBlockIndex := strings.Index(content, "```")
if synopsisIndex != -1 {
// If there's a Synopsis section, insert before it
insertPoint = synopsisIndex
} else if codeBlockIndex != -1 {
// If there's a code block, insert before it
insertPoint = codeBlockIndex
} else {
// Default to inserting after the description
descStart := cmdNameIndex + len(cmdNameLine)
nextNewline := strings.Index(content[descStart:], "\n\n")
if nextNewline != -1 {
insertPoint = descStart + nextNewline + 1
} else {
// Fallback to end of file
insertPoint = len(content)
}
}
stabilityInfo := fmt.Sprintf("\n> **Stability**: %s\n\n", stability)
return content[:insertPoint] + stabilityInfo + content[insertPoint:]
}
}
// If no stability annotation or couldn't find insertion point, return content unchanged
return content
}
// Generates cobra docs of the cmd
func main() {
if err := cmd.GenerateDoc(docPath); err != nil {
log.Fatal(err)
linkHandler := func(name string) string {
return name
}
filePrepender := func(filename string) string {
return "<!-- markdownlint-disable-file -->\n<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->\n"
}
// Generate the markdown documentation
if err := doc.GenMarkdownTreeCustom(cmd.GetRootCmd(), docPath, filePrepender, linkHandler); err != nil {
fmt.Fprintf(os.Stderr, "error generating docs: %v\n", err)
os.Exit(1)
}
// Apply the content modifier to all generated files
// This is needed because Cobra doesn't expose a way to modify content during generation
applyContentModifierToFiles(cmd.GetRootCmd(), docPath)
}
// applyContentModifierToFiles applies our content modifier to all generated markdown files
func applyContentModifierToFiles(root *cobra.Command, docPath string) {
// Process the root command
processCommandFile(root, fmt.Sprintf("%s/%s.md", docPath, root.Name()))
// Process all descendant commands recursively
processCommandTree(root, docPath)
}
// processCommandFile applies the content modifier to a single command's markdown file
func processCommandFile(cmd *cobra.Command, filePath string) {
content, err := os.ReadFile(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading file %s: %v\n", filePath, err)
return
}
// Apply our content modifier
modifiedContent := addStabilityToMarkdown(cmd, string(content))
// Only write the file if content was modified
if modifiedContent != string(content) {
err = os.WriteFile(filePath, []byte(modifiedContent), 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "error writing file %s: %v\n", filePath, err)
}
}
}
// processCommandTree recursively processes all commands in the command tree
func processCommandTree(cmd *cobra.Command, docPath string) {
for _, subCmd := range cmd.Commands() {
if !subCmd.IsAvailableCommand() || subCmd.IsAdditionalHelpTopicCommand() {
continue
}
// Calculate the filename for this command
fileName := getMarkdownFilename(cmd, subCmd)
filePath := fmt.Sprintf("%s/%s", docPath, fileName)
// Process this command's file
processCommandFile(subCmd, filePath)
// Process its children
processCommandTree(subCmd, docPath)
}
}
// getMarkdownFilename determines the markdown filename for a command based on its path
func getMarkdownFilename(parent *cobra.Command, cmd *cobra.Command) string {
if parent.Name() == "openfeature" {
return fmt.Sprintf("openfeature_%s.md", cmd.Name())
}
return fmt.Sprintf("openfeature_%s_%s.md", parent.Name(), cmd.Name())
}

99
go.mod
View File

@ -1,39 +1,86 @@
module github.com/open-feature/cli
go 1.22.5
go 1.23.0
require (
dagger.io/dagger v0.18.12
github.com/google/go-cmp v0.7.0
github.com/iancoleman/strcase v0.3.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/spf13/afero v1.11.0
github.com/invopop/jsonschema v0.13.0
github.com/pterm/pterm v0.12.81
github.com/spf13/afero v1.14.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
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.26.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
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/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.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/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.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.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/google/go-cmp v0.6.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // 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-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-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
)

294
go.sum
View File

@ -1,83 +1,271 @@
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=
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
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=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
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/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/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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/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/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=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
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.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/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=
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/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
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.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.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.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
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.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
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.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.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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
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/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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/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=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
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-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=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.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.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.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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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)
})
}
}

89
internal/cmd/config.go Normal file
View File

@ -0,0 +1,89 @@
package cmd
import (
"fmt"
"strings"
"github.com/open-feature/cli/internal/logger"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// initializeConfig reads in config file and ENV variables if set.
// It applies configuration values to command flags based on hierarchical priority.
func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
v := viper.New()
// Set the config file name and path
v.SetConfigName(".openfeature")
v.AddConfigPath(".")
logger.Default.Debug("Looking for .openfeature config file in current directory")
// Read the config file
if err := v.ReadInConfig(); err != nil {
// It's okay if there isn't a config file
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
logger.Default.Debug("No config file found, using defaults and environment variables")
} else {
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) {
cmdLineFlags[f.Name] = true
logger.Default.Debug(fmt.Sprintf("Flag set via command line: %s=%s", f.Name, f.Value.String()))
})
// Apply the configuration values
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// Skip if flag was set on command line
if cmdLineFlags[f.Name] {
logger.Default.Debug(fmt.Sprintf("Skipping config for %s: already set via command line", f.Name))
return
}
// Build configuration paths from most specific to least specific
configPaths := []string{}
// Check the most specific path (e.g., generate.go.package-name)
if bindPrefix != "" {
configPaths = append(configPaths, bindPrefix+"."+f.Name)
// Check parent paths (e.g., generate.package-name)
parts := strings.Split(bindPrefix, ".")
for i := len(parts) - 1; i > 0; i-- {
parentPath := strings.Join(parts[:i], ".") + "." + f.Name
configPaths = append(configPaths, parentPath)
}
}
// Check the base path (e.g., package-name)
configPaths = append(configPaths, f.Name)
logger.Default.Debug(fmt.Sprintf("Looking for config value for flag %s in paths: %s", f.Name, strings.Join(configPaths, ", ")))
// Try each path in order until we find a match
for _, path := range configPaths {
if v.IsSet(path) {
val := v.Get(path)
err := f.Value.Set(fmt.Sprintf("%v", val))
if err != nil {
logger.Default.Debug(fmt.Sprintf("Error setting flag %s from config: %v", f.Name, err))
} else {
logger.Default.Debug(fmt.Sprintf("Set flag %s=%s from config path %s", f.Name, val, path))
break
}
}
}
// Log the final value for the flag
logger.Default.Debug(fmt.Sprintf("Final flag value: %s=%s", f.Name, f.Value.String()))
})
return nil
}

172
internal/cmd/config_test.go Normal file
View File

@ -0,0 +1,172 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func setupTestCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "test",
}
// Add some test flags
cmd.Flags().String("output", "", "output path")
cmd.Flags().String("package-name", "default", "package name")
return cmd
}
// setupConfigFileForTest creates a temporary directory with a config file
// and changes the working directory to it.
// Returns the original working directory and temp directory path for cleanup.
func setupConfigFileForTest(t *testing.T, configContent string) (string, string) {
// Create a temporary config file
tmpDir, err := os.MkdirTemp("", "config-test")
if err != nil {
t.Fatal(err)
}
configPath := filepath.Join(tmpDir, ".openfeature.yaml")
err = os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatal(err)
}
// Change to the temporary directory so the config file can be found
originalDir, _ := os.Getwd()
err = os.Chdir(tmpDir)
if err != nil {
t.Fatal(err)
}
return originalDir, tmpDir
}
func TestRootCommandIgnoresUnrelatedConfig(t *testing.T) {
configContent := `
generate:
output: output-from-generate
`
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
defer func() {
_ = os.Chdir(originalDir)
_ = os.RemoveAll(tmpDir)
}()
rootCmd := setupTestCommand()
err := initializeConfig(rootCmd, "")
assert.NoError(t, err)
assert.Equal(t, "", rootCmd.Flag("output").Value.String(),
"Root command should not get output config from unrelated sections")
}
func TestGenerateCommandGetsGenerateConfig(t *testing.T) {
configContent := `
generate:
output: output-from-generate
`
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
defer func() {
_ = os.Chdir(originalDir)
_ = os.RemoveAll(tmpDir)
}()
generateCmd := setupTestCommand()
err := initializeConfig(generateCmd, "generate")
assert.NoError(t, err)
assert.Equal(t, "output-from-generate", generateCmd.Flag("output").Value.String(),
"Generate command should get generate.output value")
}
func TestSubcommandGetsSpecificConfig(t *testing.T) {
configContent := `
generate:
output: output-from-generate
go:
output: output-from-go
package-name: fromconfig
`
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
defer func() {
_ = os.Chdir(originalDir)
_ = os.RemoveAll(tmpDir)
}()
goCmd := setupTestCommand()
err := initializeConfig(goCmd, "generate.go")
assert.NoError(t, err)
assert.Equal(t, "output-from-go", goCmd.Flag("output").Value.String(),
"Go command should get generate.go.output, not generate.output")
assert.Equal(t, "fromconfig", goCmd.Flag("package-name").Value.String(),
"Go command should get generate.go.package-name")
}
func TestSubcommandInheritsFromParent(t *testing.T) {
configContent := `
generate:
output: output-from-generate
`
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
defer func() {
_ = os.Chdir(originalDir)
_ = os.RemoveAll(tmpDir)
}()
otherCmd := setupTestCommand()
err := initializeConfig(otherCmd, "generate.other")
assert.NoError(t, err)
assert.Equal(t, "output-from-generate", otherCmd.Flag("output").Value.String(),
"Other command should inherit generate.output when no specific config exists")
}
func TestCommandLineOverridesConfig(t *testing.T) {
// Create a temporary config file
tmpDir, err := os.MkdirTemp("", "config-test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = os.RemoveAll(tmpDir)
}()
configPath := filepath.Join(tmpDir, ".openfeature.yaml")
configContent := `
generate:
output: output-from-config
`
err = os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatal(err)
}
// Change to the temporary directory so the config file can be found
originalDir, _ := os.Getwd()
err = os.Chdir(tmpDir)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = os.Chdir(originalDir)
}()
// Set up a command with a flag value already set via command line
cmd := setupTestCommand()
_ = cmd.Flags().Set("output", "output-from-cmdline")
// Initialize config
err = initializeConfig(cmd, "generate")
assert.NoError(t, err)
// Command line value should take precedence
assert.Equal(t, "output-from-cmdline", cmd.Flag("output").Value.String(),
"Command line value should override config file")
}

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

@ -0,0 +1,188 @@
package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/filesystem"
"github.com/spf13/afero"
)
// generateTestCase holds the configuration for each generate test
type generateTestCase struct {
name string // test case name
command string // generator to run
manifestGolden string // path to the golden manifest file
outputGolden string // path to the golden output file
outputPath string // output directory (optional, defaults to "output")
outputFile string // output file name
packageName string // optional, used for Go (package-name), Java (package-name) and C# (namespace)
}
func TestGenerate(t *testing.T) {
testCases := []generateTestCase{
{
name: "Go generation success",
command: "go",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_go.golden",
outputFile: "testpackage.go",
packageName: "testpackage",
},
{
name: "React generation success",
command: "react",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_react.golden",
outputFile: "openfeature.ts",
},
{
name: "NodeJS generation success",
command: "nodejs",
manifestGolden: "testdata/success_manifest.golden",
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
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := GetGenerateCmd()
// global flag exists on root only.
config.AddRootFlags(cmd)
// Constant paths
const memoryManifestPath = "manifest/path.json"
// Use default output path if not specified
outputPath := tc.outputPath
if outputPath == "" {
outputPath = "output"
}
// Prepare in-memory files
fs := afero.NewMemMapFs()
filesystem.SetFileSystem(fs)
readOsFileAndWriteToMemMap(t, tc.manifestGolden, memoryManifestPath, fs)
// Prepare command arguments
args := []string{
tc.command,
"--manifest", memoryManifestPath,
"--output", outputPath,
}
// Add parameters specific to each generator
if 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)
// Run command
err := cmd.Execute()
if err != nil {
t.Error(err)
}
// Compare result
compareOutput(t, tc.outputGolden, filepath.Join(outputPath, tc.outputFile), fs)
})
}
}
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
data, err := os.ReadFile(inputPath)
if err != nil {
t.Fatalf("error reading file %q: %v", inputPath, err)
}
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err)
}
f, err := memFs.Create(memPath)
if err != nil {
t.Fatalf("error creating file %q: %v", memPath, err)
}
defer f.Close()
writtenBytes, err := f.Write(data)
if err != nil {
t.Fatalf("error writing contents to file %q: %v", memPath, err)
}
if writtenBytes != len(data) {
t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath)
}
}
// 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 {
t.Fatalf("error reading file %q: %v", testFile, err)
}
got, err := afero.ReadFile(fs, memoryOutputPath)
if err != nil {
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
}
// Convert to string arrays by splitting on newlines
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)
}
}

59
internal/cmd/init.go Normal file
View File

@ -0,0 +1,59 @@
package cmd
import (
"fmt"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/logger"
"github.com/open-feature/cli/internal/manifest"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
func GetInitCmd() *cobra.Command {
initCmd := &cobra.Command{
Use: "init",
Short: "Initialize a new project",
Long: "Initialize a new project for OpenFeature CLI.",
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "init")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
override := config.GetOverride(cmd)
manifestExists, _ := filesystem.Exists(manifestPath)
if manifestExists && !override {
logger.Default.Debug(fmt.Sprintf("Manifest file already exists at %s", manifestPath))
confirmMessage := fmt.Sprintf("An existing manifest was found at %s. Would you like to override it?", manifestPath)
shouldOverride, _ := pterm.DefaultInteractiveConfirm.Show(confirmMessage)
// Print a blank line for better readability.
pterm.Println()
if !shouldOverride {
logger.Default.Info("No changes were made.")
return nil
}
logger.Default.Debug("User confirmed override of existing manifest")
}
logger.Default.Info("Initializing project...")
err := manifest.Create(manifestPath)
if err != nil {
logger.Default.Error(fmt.Sprintf("Failed to create manifest: %v", err))
return err
}
logger.Default.FileCreated(manifestPath)
logger.Default.Success("Project initialized.")
return nil
},
}
config.AddInitFlags(initCmd)
addStabilityInfo(initCmd)
return initCmd
}

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)
}

75
internal/cmd/root.go Normal file
View File

@ -0,0 +1,75 @@
package cmd
import (
"fmt"
"os"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/logger"
"github.com/spf13/cobra"
)
var (
Version = "dev"
Commit string
Date string
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(version string, commit string, date string) {
Version = version
Commit = commit
Date = date
if err := GetRootCmd().Execute(); err != nil {
logger.Default.Error(err.Error())
os.Exit(1)
}
}
func GetRootCmd() *cobra.Command {
// Execute all parent's persistent hooks
cobra.EnableTraverseRunHooks = true
rootCmd := &cobra.Command{
Use: "openfeature",
Short: "CLI for OpenFeature.",
Long: `CLI for OpenFeature related functionalities.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
debug, _ := cmd.Flags().GetBool("debug")
logger.Default.SetDebug(debug)
logger.Default.Debug("Debug logging enabled")
return initializeConfig(cmd, "")
},
RunE: func(cmd *cobra.Command, args []string) error {
printBanner()
logger.Default.Println("")
logger.Default.Println("To see all the options, try 'openfeature --help'")
return nil
},
SilenceErrors: true,
SilenceUsage: true,
DisableSuggestions: false,
SuggestionsMinimumDistance: 2,
DisableAutoGenTag: true,
}
// Add global flags using the config package
config.AddRootFlags(rootCmd)
// Add subcommands
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 {
logger.Default.Error(fmt.Sprintf("Invalid flag: %s", err))
logger.Default.Info("Run 'openfeature --help' for usage information")
return err
})
return rootCmd
}

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

@ -1,4 +1,4 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CODEGEN, DO NOT EDIT.
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
package testpackage
import (
@ -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,
@ -86,5 +105,5 @@ var UsernameMaxLength = struct {
}
func init() {
client = openfeature.GetApiInstance().GetClient()
client = openfeature.GetApiInstance().GetClient()
}

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

@ -0,0 +1,235 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
import {
OpenFeature,
stringOrUndefined,
objectOrUndefined,
JsonValue,
} from "@openfeature/server-sdk";
import type {
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
} from "@openfeature/server-sdk";
export interface GeneratedClient {
/**
* Discount percentage applied to purchases.
*
* **Details:**
* - flag key: `discountPercentage`
* - default value: `0.15`
* - type: `number`
*
* Performs a flag evaluation that returns a number.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<number>} Flag evaluation response
*/
discountPercentage(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number>;
/**
* Discount percentage applied to purchases.
*
* **Details:**
* - flag key: `discountPercentage`
* - default value: `0.15`
* - type: `number`
*
* 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<number>>} Flag evaluation details response
*/
discountPercentageDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<number>>;
/**
* Controls whether Feature A is enabled.
*
* **Details:**
* - flag key: `enableFeatureA`
* - default value: `false`
* - type: `boolean`
*
* Performs a flag evaluation that returns a boolean.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<boolean>} Flag evaluation response
*/
enableFeatureA(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<boolean>;
/**
* Controls whether Feature A is enabled.
*
* **Details:**
* - flag key: `enableFeatureA`
* - default value: `false`
* - type: `boolean`
*
* 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<boolean>>} Flag evaluation details response
*/
enableFeatureADetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<boolean>>;
/**
* The message to use for greeting users.
*
* **Details:**
* - flag key: `greetingMessage`
* - default value: `Hello there!`
* - type: `string`
*
* Performs a flag evaluation that returns a string.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<string>} Flag evaluation response
*/
greetingMessage(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<string>;
/**
* The message to use for greeting users.
*
* **Details:**
* - flag key: `greetingMessage`
* - default value: `Hello there!`
* - type: `string`
*
* 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<string>>} Flag evaluation details response
*/
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.
*
* **Details:**
* - flag key: `usernameMaxLength`
* - default value: `50`
* - type: `number`
*
* Performs a flag evaluation that returns a number.
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
* @param {FlagEvaluationOptions} options Additional flag evaluation options
* @returns {Promise<number>} Flag evaluation response
*/
usernameMaxLength(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number>;
/**
* Maximum allowed length for usernames.
*
* **Details:**
* - flag key: `usernameMaxLength`
* - default value: `50`
* - type: `number`
*
* 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<number>>} Flag evaluation details response
*/
usernameMaxLengthDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<number>>;
}
/**
* A factory function that returns a generated client that not bound to a domain.
* It was generated using the OpenFeature CLI and is compatible with `@openfeature/server-sdk`.
*
* All domainless or unbound clients use the default provider set via {@link OpenFeature.setProvider}.
* @param {EvaluationContext} context Evaluation context that should be set on the client to used during flag evaluations
* @returns {GeneratedClient} Generated OpenFeature Client
*/
export function getGeneratedClient(context?: EvaluationContext): GeneratedClient
/**
* A factory function that returns a domain-bound generated client that was
* created using the OpenFeature CLI and is compatible with the `@openfeature/server-sdk`.
*
* If there is already a provider bound to this domain via {@link OpenFeature.setProvider}, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
* @param {string} domain An identifier which logically binds clients with providers
* @param {EvaluationContext} context Evaluation context that should be set on the client to used during flag evaluations
* @returns {GeneratedClient} Generated OpenFeature Client
*/
export function getGeneratedClient(domain: string, context?: EvaluationContext): GeneratedClient
export function getGeneratedClient(domainOrContext?: string | EvaluationContext, contextOrUndefined?: EvaluationContext): GeneratedClient {
const domain = stringOrUndefined(domainOrContext);
const context =
objectOrUndefined<EvaluationContext>(domainOrContext) ??
objectOrUndefined<EvaluationContext>(contextOrUndefined);
const client = domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context)
return {
discountPercentage: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number> => {
return client.getNumberValue("discountPercentage", 0.15, context, options);
},
discountPercentageDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<number>> => {
return client.getNumberDetails("discountPercentage", 0.15, context, options);
},
enableFeatureA: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<boolean> => {
return client.getBooleanValue("enableFeatureA", false, context, options);
},
enableFeatureADetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<boolean>> => {
return client.getBooleanDetails("enableFeatureA", false, context, options);
},
greetingMessage: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<string> => {
return client.getStringValue("greetingMessage", "Hello there!", context, options);
},
greetingMessageDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<string>> => {
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);
},
usernameMaxLengthDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<number>> => {
return client.getNumberDetails("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";
/**
@ -15,7 +16,7 @@ import {
* - default value: `0.15`
* - type: `number`
*/
export const useDiscountPercentage = (options: ReactFlagEvaluationOptions) => {
export const useDiscountPercentage = (options?: ReactFlagEvaluationOptions) => {
return useFlag("discountPercentage", 0.15, options);
};
@ -30,7 +31,7 @@ export const useDiscountPercentage = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseDiscountPercentage = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseDiscountPercentage = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("discountPercentage", 0.15, options);
};
@ -42,7 +43,7 @@ export const useSuspenseDiscountPercentage = (options: ReactFlagEvaluationNoSusp
* - default value: `false`
* - type: `boolean`
*/
export const useEnableFeatureA = (options: ReactFlagEvaluationOptions) => {
export const useEnableFeatureA = (options?: ReactFlagEvaluationOptions) => {
return useFlag("enableFeatureA", false, options);
};
@ -57,7 +58,7 @@ export const useEnableFeatureA = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseEnableFeatureA = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseEnableFeatureA = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("enableFeatureA", false, options);
};
@ -69,7 +70,7 @@ export const useSuspenseEnableFeatureA = (options: ReactFlagEvaluationNoSuspense
* - default value: `Hello there!`
* - type: `string`
*/
export const useGreetingMessage = (options: ReactFlagEvaluationOptions) => {
export const useGreetingMessage = (options?: ReactFlagEvaluationOptions) => {
return useFlag("greetingMessage", "Hello there!", options);
};
@ -84,10 +85,37 @@ export const useGreetingMessage = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseGreetingMessage = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
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.
*
@ -96,7 +124,7 @@ export const useSuspenseGreetingMessage = (options: ReactFlagEvaluationNoSuspens
* - default value: `50`
* - type: `number`
*/
export const useUsernameMaxLength = (options: ReactFlagEvaluationOptions) => {
export const useUsernameMaxLength = (options?: ReactFlagEvaluationOptions) => {
return useFlag("usernameMaxLength", 50, options);
};
@ -111,6 +139,6 @@ export const useUsernameMaxLength = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseUsernameMaxLength = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseUsernameMaxLength = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("usernameMaxLength", 50, options);
};

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!"
}
}
}

20
internal/cmd/utils.go Normal file
View File

@ -0,0 +1,20 @@
package cmd
import "github.com/pterm/pterm"
func printBanner() {
ivrit := `
___ _____ _
/ _ \ _ __ ___ _ __ | ___|__ __ _| |_ _ _ _ __ ___
| | | | '_ \ / _ \ '_ \| |_ / _ \/ _` + "`" + ` | __| | | | '__/ _ \
| |_| | |_) | __/ | | | _| __/ (_| | |_| |_| | | | __/
\___/| .__/ \___|_| |_|_| \___|\__,_|\__|\__,_|_| \___|
|_|
CLI
`
pterm.Println(ivrit)
pterm.Println()
pterm.Printf("version: %s | compiled: %s\n", pterm.LightGreen(Version), pterm.LightGreen(Date))
pterm.Println(pterm.Cyan("🔗 https://openfeature.dev | https://github.com/open-feature/cli"))
}

41
internal/cmd/version.go Normal file
View File

@ -0,0 +1,41 @@
package cmd
import (
"fmt"
"runtime/debug"
"github.com/open-feature/cli/internal/logger"
"github.com/spf13/cobra"
)
func GetVersionCmd() *cobra.Command {
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version number of the OpenFeature CLI",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
if Version == "dev" {
logger.Default.Debug("Development version detected, attempting to get build info")
details, ok := debug.ReadBuildInfo()
if ok && details.Main.Version != "" && details.Main.Version != "(devel)" {
Version = details.Main.Version
for _, i := range details.Settings {
if i.Key == "vcs.time" {
Date = i.Value
logger.Default.Debug(fmt.Sprintf("Found build date: %s", Date))
}
if i.Key == "vcs.revision" {
Commit = i.Value
logger.Default.Debug(fmt.Sprintf("Found commit: %s", Commit))
}
}
}
}
versionInfo := fmt.Sprintf("OpenFeature CLI: %s (%s), built at: %s", Version, Commit, Date)
logger.Default.Info(versionInfo)
},
}
return versionCmd
}

100
internal/config/flags.go Normal file
View File

@ -0,0 +1,100 @@
package config
import (
"github.com/spf13/cobra"
)
// Flag name constants to avoid duplication
const (
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"
DefaultCSharpNamespace = "OpenFeature"
DefaultJavaPackageName = "com.example.openfeature"
)
// AddRootFlags adds the common flags to the given command
func AddRootFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringP(ManifestFlagName, "m", DefaultManifestPath, "Path to the flag manifest")
cmd.PersistentFlags().Bool(NoInputFlagName, false, "Disable interactive prompts")
cmd.PersistentFlags().Bool(DebugFlagName, false, "Enable debug logging")
}
// AddGenerateFlags adds the common generate flags to the given command
func AddGenerateFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringP(OutputFlagName, "o", DefaultOutputPath, "Path to where the generated files should be saved")
}
// AddGoGenerateFlags adds the go generator specific flags to the given command
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")
}
// GetManifestPath gets the manifest path from the given command
func GetManifestPath(cmd *cobra.Command) string {
manifestPath, _ := cmd.Flags().GetString(ManifestFlagName)
return manifestPath
}
// GetOutputPath gets the output path from the given command
func GetOutputPath(cmd *cobra.Command) string {
outputPath, _ := cmd.Flags().GetString(OutputFlagName)
return outputPath
}
// GetGoPackageName gets the Go package name from the given command
func GetGoPackageName(cmd *cobra.Command) string {
goPackageName, _ := cmd.Flags().GetString(GoPackageFlagName)
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)
return noInput
}
// GetOverride gets the override flag from the given command
func GetOverride(cmd *cobra.Command) bool {
override, _ := cmd.Flags().GetBool(OverrideFlagName)
return override
}

View File

@ -2,16 +2,65 @@
package filesystem
import (
"github.com/open-feature/cli/internal/flagkeys"
"fmt"
"os"
"path/filepath"
"github.com/spf13/afero"
"github.com/spf13/viper"
)
var viperKey = "filesystem"
// Get the filesystem interface from the viper configuration.
// If the filesystem interface is not set, the default filesystem interface is returned.
func FileSystem() afero.Fs {
return viper.Get(flagkeys.FileSystem).(afero.Fs)
return viper.Get(viperKey).(afero.Fs)
}
// Set the filesystem interface in the viper configuration.
// This is useful for testing purposes.
func SetFileSystem(fs afero.Fs) {
viper.Set(viperKey, fs)
}
// Writes data to a file at the given path using the filesystem interface.
// If the file does not exist, it will be created, including all necessary directories.
// If the file exists, it will be overwritten.
func WriteFile(path string, data []byte) error {
fs := FileSystem()
if err := fs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
f, err := fs.Create(path)
if err != nil {
return fmt.Errorf("error creating file %q: %v", path, err)
}
defer f.Close()
writtenBytes, err := f.Write(data)
if err != nil {
return fmt.Errorf("error writing contents to file %q: %v", path, err)
}
if writtenBytes != len(data) {
return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", path)
}
return nil
}
// Checks if a file exists at the given path using the filesystem interface.
func Exists(path string) (bool, error) {
fs := FileSystem()
_, err := fs.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func init() {
viper.SetDefault(flagkeys.FileSystem, afero.NewOsFs())
viper.SetDefault(viperKey, afero.NewOsFs())
}

View File

@ -1,24 +0,0 @@
// Package commonflags contains keys for all command-line flags related to openfeature CLI.
package flagkeys
import "github.com/spf13/viper"
const (
// `generate` flags:
// FlagManifestPath is the key for the flag that stores the flag manifest path.
FlagManifestPath = "flag_manifest_path"
// OutputPath is the key for the flag that stores the output path.
OutputPath = "output_path"
// `generate go` flags:
// GoPackageName is the key for the flag that stores the Golang package name.
GoPackageName = "package_name"
//internal keys:
// FileSystem is the key for the flag that stores the filesystem interface.
FileSystem = "filesystem"
)
func init() {
viper.SetDefault(FileSystem, "local")
}

175
internal/flagset/flagset.go Normal file
View File

@ -0,0 +1,175 @@
package flagset
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/manifest"
"github.com/spf13/afero"
)
// FlagType are the primitive types of flags.
type FlagType int
// Collection of the different kinds of flag types
const (
UnknownFlagType FlagType = iota
IntType
FloatType
BoolType
StringType
ObjectType
)
func (f FlagType) String() string {
switch f {
case IntType:
return "int"
case FloatType:
return "float"
case BoolType:
return "bool"
case StringType:
return "string"
case ObjectType:
return "object"
default:
return "unknown"
}
}
type Flag struct {
Key string
Type FlagType
Description string
DefaultValue any
}
type Flagset struct {
Flags []Flag
}
// Loads, validates, and unmarshals the manifest file at the given path into a flagset
func Load(manifestPath string) (*Flagset, error) {
fs := filesystem.FileSystem()
data, err := afero.ReadFile(fs, manifestPath)
if err != nil {
return nil, fmt.Errorf("error reading contents from file %q", manifestPath)
}
validationErrors, err := manifest.Validate(data)
if err != nil {
return nil, err
} else if len(validationErrors) > 0 {
return nil, errors.New(FormatValidationError(validationErrors))
}
var flagset Flagset
if err := json.Unmarshal(data, &flagset); err != nil {
return nil, fmt.Errorf("error unmarshaling JSON: %v", validationErrors)
}
return &flagset, nil
}
// Filter removes flags from the Flagset that are of unsupported types.
func (fs *Flagset) Filter(unsupportedFlagTypes map[FlagType]bool) *Flagset {
var filtered Flagset
for _, flag := range fs.Flags {
if !unsupportedFlagTypes[flag.Type] {
filtered.Flags = append(filtered.Flags, flag)
}
}
return &filtered
}
// UnmarshalJSON unmarshals the JSON data into a Flagset. It is used by json.Unmarshal.
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"`
} `json:"flags"`
}
if err := json.Unmarshal(data, &manifest); err != nil {
return err
}
for key, flag := range manifest.Flags {
var flagType FlagType
switch flag.FlagType {
case "integer":
flagType = IntType
case "float":
flagType = FloatType
case "boolean":
flagType = BoolType
case "string":
flagType = StringType
case "object":
flagType = ObjectType
default:
return errors.New("unknown flag type")
}
fs.Flags = append(fs.Flags, Flag{
Key: key,
Type: flagType,
Description: flag.Description,
DefaultValue: flag.DefaultValue,
})
}
// Ensure consistency of order of flag generation.
sort.Slice(fs.Flags, func(i, j int) bool {
return fs.Flags[i].Key < fs.Flags[j].Key
})
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

@ -1,64 +0,0 @@
// Package generate contains the top level functions used for generating flag accessors.
package generate
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"text/template"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/flagkeys"
"github.com/open-feature/cli/internal/generate/manifestutils"
"github.com/open-feature/cli/internal/generate/types"
"github.com/spf13/viper"
)
// GenerateFile receives data for the Go template engine and outputs the contents to the file.
// Intended to be invoked by each language generator with appropriate data.
func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataInterface) error {
contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents)
if err != nil {
return fmt.Errorf("error initializing template: %v", err)
}
var buf bytes.Buffer
if err := contentsTmpl.Execute(&buf, data); err != nil {
return fmt.Errorf("error executing template: %v", err)
}
outputPath := data.BaseTmplDataInfo().OutputPath
fs := filesystem.FileSystem()
if err := fs.MkdirAll(filepath.Dir(outputPath), os.ModePerm); err != nil {
return err
}
f, err := fs.Create(path.Join(outputPath))
if err != nil {
return fmt.Errorf("error creating file %q: %v", outputPath, err)
}
defer f.Close()
writtenBytes, err := f.Write(buf.Bytes())
if err != nil {
return fmt.Errorf("error writing contents to file %q: %v", outputPath, err)
}
if writtenBytes != buf.Len() {
return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath)
}
return nil
}
// Takes as input a generator and outputs file with the appropriate flag accessors.
// The flag data is taken from the provided flag manifest.
func CreateFlagAccessors(gen types.Generator) error {
bt, err := manifestutils.LoadData(viper.GetString(flagkeys.FlagManifestPath), gen.SupportedFlagTypes())
if err != nil {
return fmt.Errorf("error loading flag manifest: %v", err)
}
input := types.Input{
BaseData: bt,
}
return gen.Generate(input)
}

View File

@ -1,113 +0,0 @@
// Package manifestutils contains useful functions for loading the flag manifest.
package manifestutils
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/flagkeys"
"github.com/open-feature/cli/internal/generate/types"
flagmanifest "github.com/open-feature/cli/schema/v0"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/spf13/afero"
"github.com/spf13/viper"
)
// LoadData loads the data from the flag manifest.
func LoadData(manifestPath string, supportedFlagTypes map[types.FlagType]bool) (*types.BaseTmplData, error) {
fs := filesystem.FileSystem()
data, err := afero.ReadFile(fs, manifestPath)
if err != nil {
return nil, fmt.Errorf("error reading contents from file %q", manifestPath)
}
unfilteredData, err := unmarshalFlagManifest(data)
if err != nil {
return nil, err
}
filteredData := filterUnsupportedFlags(unfilteredData, supportedFlagTypes)
return filteredData, nil
}
func filterUnsupportedFlags(unfilteredData *types.BaseTmplData, supportedFlagTypes map[types.FlagType]bool) *types.BaseTmplData {
filteredData := &types.BaseTmplData{
OutputPath: unfilteredData.OutputPath,
}
for _, flagData := range unfilteredData.Flags {
if supportedFlagTypes[flagData.Type] {
filteredData.Flags = append(filteredData.Flags, flagData)
}
}
return filteredData
}
var stringToFlagType = map[string]types.FlagType{
"string": types.StringType,
"boolean": types.BoolType,
"float": types.FloatType,
"integer": types.IntType,
"object": types.ObjectType,
}
func getDefaultValue(defaultValue interface{}, flagType types.FlagType) string {
switch flagType {
case types.BoolType:
return strconv.FormatBool(defaultValue.(bool))
case types.IntType:
//the conversion to float64 instead of integer typically occurs
//due to how JSON is parsed in Go. In Go's encoding/json package,
//all JSON numbers are unmarshaled into float64 by default when decoding into an interface{}.
return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64)
case types.FloatType:
return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64)
case types.StringType:
return defaultValue.(string)
default:
return ""
}
}
func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) {
dynamic := make(map[string]interface{})
err := json.Unmarshal(data, &dynamic)
if err != nil {
return nil, fmt.Errorf("error unmarshalling JSON: %v", err)
}
sch, err := jsonschema.CompileString(flagmanifest.SchemaPath, flagmanifest.Schema)
if err != nil {
return nil, fmt.Errorf("error compiling JSON schema: %v", err)
}
if err = sch.Validate(dynamic); err != nil {
return nil, fmt.Errorf("error validating JSON schema: %v", err)
}
// All casts can be done directly since the JSON is already validated by the schema.
iFlags := dynamic["flags"]
flags := iFlags.(map[string]interface{})
btData := types.BaseTmplData{
OutputPath: viper.GetString(flagkeys.OutputPath),
}
for flagKey, iFlagData := range flags {
flagData := iFlagData.(map[string]interface{})
flagTypeString := flagData["flagType"].(string)
flagType := stringToFlagType[flagTypeString]
docs := flagData["description"].(string)
defaultValue := getDefaultValue(flagData["defaultValue"], flagType)
btData.Flags = append(btData.Flags, &types.FlagTmplData{
Name: flagKey,
Type: flagType,
DefaultValue: defaultValue,
Docs: docs,
})
}
// Ensure consistency of order of flag generation.
sort.Slice(btData.Flags, func(i, j int) bool {
return btData.Flags[i].Name < btData.Flags[j].Name
})
return &btData, nil
}

View File

@ -1,134 +0,0 @@
package golang
import (
_ "embed"
"sort"
"strconv"
"text/template"
"github.com/open-feature/cli/internal/generate"
"github.com/open-feature/cli/internal/generate/types"
"github.com/iancoleman/strcase"
)
// TmplData contains the Golang-specific data and the base data for the codegen.
type TmplData struct {
*types.BaseTmplData
GoPackage string
}
type genImpl struct {
goPackage string
}
// BaseTmplDataInfo provides the base template data for the codegen.
func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData {
return td.BaseTmplData
}
// supportedFlagTypes is the flag types supported by the Go template.
var supportedFlagTypes = map[types.FlagType]bool{
types.FloatType: true,
types.StringType: true,
types.IntType: true,
types.BoolType: true,
types.ObjectType: false,
}
func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool {
return supportedFlagTypes
}
//go:embed golang.tmpl
var golangTmpl string
// Go Funcs BEGIN
func flagVarName(flagName string) string {
return strcase.ToCamel(flagName)
}
func flagInitParam(flagName string) string {
return strconv.Quote(flagName)
}
func openFeatureType(t types.FlagType) string {
switch t {
case types.IntType:
return "Int"
case types.FloatType:
return "Float"
case types.BoolType:
return "Boolean"
case types.StringType:
return "String"
default:
return ""
}
}
func supportImports(flags []*types.FlagTmplData) []string {
var res []string
if len(flags) > 0 {
res = append(res, "\"context\"")
res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"")
}
sort.Strings(res)
return res
}
func defaultValueLiteral(flag *types.FlagTmplData) string {
switch flag.Type {
case types.StringType:
return strconv.Quote(flag.DefaultValue)
default:
return flag.DefaultValue
}
}
func typeString(flagType types.FlagType) string {
switch flagType {
case types.StringType:
return "string"
case types.IntType:
return "int64"
case types.BoolType:
return "bool"
case types.FloatType:
return "float64"
default:
return ""
}
}
// Go Funcs END
// Generate generates the Go flag accessors for OpenFeature.
func (g *genImpl) Generate(input types.Input) error {
funcs := template.FuncMap{
"FlagVarName": flagVarName,
"FlagInitParam": flagInitParam,
"OpenFeatureType": openFeatureType,
"SupportImports": supportImports,
"DefaultValueLiteral": defaultValueLiteral,
"TypeString": typeString,
}
td := TmplData{
BaseTmplData: input.BaseData,
GoPackage: g.goPackage,
}
return generate.GenerateFile(funcs, golangTmpl, &td)
}
// Params are parameters for creating a Generator
type Params struct {
GoPackage string
}
// NewGenerator creates a generator for Go.
func NewGenerator(params Params) types.Generator {
return &genImpl{
goPackage: params.GoPackage,
}
}

View File

@ -1,44 +0,0 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CODEGEN, DO NOT EDIT.
package {{.GoPackage}}
import (
{{- range $_, $p := SupportImports .Flags}}
{{$p}}
{{- end}}
)
type BooleanProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (bool, error)
type BooleanProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.BooleanEvaluationDetails, error)
type FloatProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (float64, error)
type FloatProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.FloatEvaluationDetails, error)
type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (int64, error)
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)
var client openfeature.IClient = nil
{{- range .Flags}}
// {{.Docs}}
var {{FlagVarName .Name}} = struct {
// Value returns the value of the flag {{FlagVarName .Name}},
// as well as the evaluation error, if present.
Value {{OpenFeatureType .Type}}Provider
// ValueWithDetails returns the value of the flag {{FlagVarName .Name}},
// the evaluation error, if any, and the evaluation details.
ValueWithDetails {{OpenFeatureType .Type}}ProviderDetails
}{
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{TypeString .Type}}, error) {
return client.{{OpenFeatureType .Type}}Value(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{OpenFeatureType .Type}}EvaluationDetails, error){
return client.{{OpenFeatureType .Type}}ValueDetails(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evalCtx)
},
}
{{- end}}
func init() {
client = openfeature.GetApiInstance().GetClient()
}

View File

@ -1,92 +0,0 @@
package react
import (
_ "embed"
"strconv"
"text/template"
"github.com/open-feature/cli/internal/generate"
"github.com/open-feature/cli/internal/generate/types"
"github.com/iancoleman/strcase"
)
type TmplData struct {
*types.BaseTmplData
}
type genImpl struct {
}
// BaseTmplDataInfo provides the base template data for the codegen.
func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData {
return td.BaseTmplData
}
// supportedFlagTypes is the flag types supported by the Go template.
var supportedFlagTypes = map[types.FlagType]bool{
types.FloatType: true,
types.StringType: true,
types.IntType: true,
types.BoolType: true,
types.ObjectType: false,
}
func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool {
return supportedFlagTypes
}
//go:embed react.tmpl
var reactTmpl string
func flagVarName(flagName string) string {
return strcase.ToCamel(flagName)
}
func flagInitParam(flagName string) string {
return strconv.Quote(flagName)
}
func defaultValueLiteral(flag *types.FlagTmplData) string {
switch flag.Type {
case types.StringType:
return strconv.Quote(flag.DefaultValue)
default:
return flag.DefaultValue
}
}
func typeString(flagType types.FlagType) string {
switch flagType {
case types.StringType:
return "string"
case types.IntType, types.FloatType:
return "number"
case types.BoolType:
return "boolean"
default:
return ""
}
}
func (g *genImpl) Generate(input types.Input) error {
funcs := template.FuncMap{
"FlagVarName": flagVarName,
"FlagInitParam": flagInitParam,
"DefaultValueLiteral": defaultValueLiteral,
"TypeString": typeString,
}
td := TmplData{
BaseTmplData: input.BaseData,
}
return generate.GenerateFile(funcs, reactTmpl, &td)
}
// Params are parameters for creating a Generator
type Params struct {
}
// NewGenerator creates a generator for React.
func NewGenerator(params Params) types.Generator {
return &genImpl{}
}

View File

@ -1,36 +0,0 @@
'use client';
import {
type ReactFlagEvaluationOptions,
type ReactFlagEvaluationNoSuspenseOptions,
useFlag,
useSuspenseFlag,
} from "@openfeature/react-sdk";
{{ range .Flags}}
/**
* {{.Docs}}
*
* **Details:**
* - flag key: `{{ .Name}}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{TypeString .Type}}`
*/
export const use{{FlagVarName .Name}} = (options: ReactFlagEvaluationOptions) => {
return useFlag({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
};
/**
* {{.Docs}}
*
* **Details:**
* - flag key: `{{ .Name}}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{TypeString .Type}}`
*
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspense{{FlagVarName .Name}} = (options: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
};
{{ end}}

View File

@ -1,46 +0,0 @@
// Package types contains all the common types and interfaces for generating flag accessors.
package types
// FlagType are the primitive types of flags.
type FlagType int
// Collection of the different kinds of flag types
const (
UnknownFlagType FlagType = iota
IntType
FloatType
BoolType
StringType
ObjectType
)
// FlagTmplData is the per-flag specific data.
// It represents a common interface between Mendel source and codegen file output.
type FlagTmplData struct {
Name string
Type FlagType
DefaultValue string
Docs string
}
// BaseTmplData is the base for all OpenFeature code generation.
type BaseTmplData struct {
OutputPath string
Flags []*FlagTmplData
}
type TmplDataInterface interface {
// BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing
// all the relevant information needed for metadata construction.
BaseTmplDataInfo() *BaseTmplData
}
type Input struct {
BaseData *BaseTmplData
}
// Generator provides interface to generate language specific, strongly-typed flag accessors.
type Generator interface {
Generate(input Input) error
SupportedFlagTypes() map[FlagType]bool
}

View File

@ -0,0 +1,58 @@
# Generators
This directory contains the code generators for different programming languages. Each generator is responsible for generating code based on the OpenFeature flag manifest.
## Structure
Each generator should be placed in its own directory under `/internal/generators`. The directory should be named after the target language (e.g., `golang`, `react`).
Each generator directory should contain the following files:
- `language.go`: This file contains the implementation of the generator logic for the target language. Replace `language` with the name of the target language (e.g., `golang.go`, `react.go`).
- `language.tmpl`: This file contains the template used by the generator to produce the output code. Replace `language` with the name of the target language (e.g., `golang.tmpl`, `react.tmpl`).
## How Generators Work
Each generator consists of two main components: the `language.go` file and the `language.tmpl` file. The `language.go` file contains the logic for processing the feature flag manifest and generating the output code, while the `language.tmpl` file defines the template used to produce the final code.
### `language.go`
The `language.go` file is responsible for reading the feature flag manifest, processing the data, and applying it to the template defined in the `language.tmpl` file. This file typically includes functions for parsing the manifest, preparing the data for the template, and writing the generated code to the appropriate output files.
### `language.tmpl`
The `language.tmpl` file is a text template that defines the structure of the generated code. It uses the Go template syntax to insert data from the feature flag manifest into the appropriate places in the template. The `language.go` file processes this template and fills in the data to produce the final code.
### Example Workflow
1. The `language.go` file reads the feature flag manifest and parses the data.
2. The data is processed and prepared for the template.
3. The `language.go` file applies the data to the `language.tmpl` file using the Go template engine.
4. The generated code is written to the appropriate output files.
By following this pattern, you can create generators for different programming languages that produce consistent and reliable code based on the feature flag manifest.
## Example
Here is an example structure for a Go generator:
```
/internal/generators/
golang/
golang.go
golang.tmpl
```
## Adding a New Generator
To add a new generator, follow these steps:
1. Create a new directory under `/internal/generators` with the name of the target language.
2. Add the `language.go` and `language.tmpl` files to the new directory.
3. Implement the generator logic in the `language.go` file.
4. Create the template in the `language.tmpl` file.
5. Ensure that your generator follows the existing patterns and conventions used in the project.
6. Write tests for your generator to ensure it works as expected.
7. Update the documentation to include information about your new generator.
We appreciate your contributions and look forward to seeing your new generators!

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

@ -0,0 +1,42 @@
package generators
import (
"strconv"
"strings"
"text/template"
"github.com/iancoleman/strcase"
"golang.org/x/text/cases"
)
func defaultFuncs() template.FuncMap {
// Update the contributing doc when adding a new function
return template.FuncMap{
// Remapping ToCamel to ToPascal to match the expected behavior
// Ref: https://github.com/iancoleman/strcase/issues/53
"ToPascal": strcase.ToCamel,
// Remapping ToLowerCamel to ToCamel to match the expected behavior
// Ref: See above
"ToCamel": strcase.ToLowerCamel,
"ToKebab": strcase.ToKebab,
"ToScreamingKebab": strcase.ToScreamingKebab,
"ToSnake": strcase.ToSnake,
"ToScreamingSnake": strcase.ToScreamingSnake,
"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)
}
return input
},
}
}
func init() {
// results in "Api" using ToCamel("API")
// results in "api" using ToLowerCamel("API")
strcase.ConfigureAcronym("API", "api")
}

View File

@ -0,0 +1,76 @@
package generators
import (
"bytes"
"fmt"
"path/filepath"
"text/template"
"maps"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/logger"
)
// Represents the stability level of a generator
type Stability string
const (
Alpha Stability = "alpha"
Beta Stability = "beta"
Stable Stability = "stable"
)
type CommonGenerator struct {
Flagset *flagset.Flagset
}
type Params[T any] struct {
OutputPath string
Custom T
}
type TemplateData struct {
CommonGenerator
Params[any]
}
// NewGenerator creates a new generator
func NewGenerator(flagset *flagset.Flagset, UnsupportedFlagTypes map[flagset.FlagType]bool) *CommonGenerator {
return &CommonGenerator{
Flagset: flagset.Filter(UnsupportedFlagTypes),
}
}
func (g *CommonGenerator) GenerateFile(customFunc template.FuncMap, tmpl string, params *Params[any], name string) error {
funcs := defaultFuncs()
maps.Copy(funcs, customFunc)
logger.Default.Debug(fmt.Sprintf("Generating file: %s", name))
generatorTemplate, err := template.New("generator").Funcs(funcs).Parse(tmpl)
if err != nil {
return fmt.Errorf("error initializing template: %v", err)
}
var buf bytes.Buffer
data := TemplateData{
CommonGenerator: *g,
Params: *params,
}
if err := generatorTemplate.Execute(&buf, data); err != nil {
return fmt.Errorf("error executing template: %v", err)
}
fullPath := filepath.Join(params.OutputPath, name)
if err := filesystem.WriteFile(fullPath, buf.Bytes()); err != nil {
logger.Default.FileFailed(fullPath, err)
return err
}
// Log successful file creation
logger.Default.FileCreated(fullPath)
return nil
}

View File

@ -0,0 +1,151 @@
package golang
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"sort"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type GolangGenerator struct {
generators.CommonGenerator
}
type Params struct {
GoPackage string
}
//go:embed golang.tmpl
var golangTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
return "Int"
case flagset.FloatType:
return "Float"
case flagset.BoolType:
return "Boolean"
case flagset.StringType:
return "String"
case flagset.ObjectType:
return "Object"
default:
return ""
}
}
func typeString(flagType flagset.FlagType) string {
switch flagType {
case flagset.StringType:
return "string"
case flagset.IntType:
return "int64"
case flagset.BoolType:
return "bool"
case flagset.FloatType:
return "float64"
case flagset.ObjectType:
return "map[string]any"
default:
return ""
}
}
func supportImports(flags []flagset.Flag) []string {
var res []string
if len(flags) > 0 {
res = append(res, "\"context\"")
res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"")
}
sort.Strings(res)
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]{
OutputPath: params.OutputPath,
Custom: Params{
GoPackage: params.Custom.GoPackage,
},
}
return g.GenerateFile(funcs, golangTmpl, newParams, params.Custom.GoPackage+".go")
}
// NewGenerator creates a generator for Go.
func NewGenerator(fs *flagset.Flagset) *GolangGenerator {
return &GolangGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,45 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
package {{ .Params.Custom.GoPackage }}
import (
{{- range $_, $p := SupportImports .Flagset.Flags}}
{{$p}}
{{- end}}
)
type BooleanProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (bool, error)
type BooleanProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.BooleanEvaluationDetails, error)
type FloatProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (float64, error)
type FloatProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.FloatEvaluationDetails, error)
type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (int64, error)
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
{{- range .Flagset.Flags }}
// {{.Description}}
var {{ .Key | ToPascal }} = struct {
// Value returns the value of the flag {{ .Key | ToPascal }},
// as well as the evaluation error, if present.
Value {{ .Type | OpenFeatureType }}Provider
// ValueWithDetails returns the value of the flag {{ .Key | ToPascal }},
// the evaluation error, if any, and the evaluation details.
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
}{
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.{{- 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}}
func init() {
client = openfeature.GetApiInstance().GetClient()
}

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

@ -0,0 +1,86 @@
package generators
import (
"sort"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
// GeneratorCreator is a function that creates a generator command
type GeneratorCreator func() *cobra.Command
// GeneratorInfo contains metadata about a generator
type GeneratorInfo struct {
Name string
Description string
Stability Stability
Creator GeneratorCreator
}
// GeneratorManager maintains a registry of available generators
type GeneratorManager struct {
generators map[string]GeneratorInfo
}
// NewGeneratorManager creates a new generator manager
func NewGeneratorManager() *GeneratorManager {
return &GeneratorManager{
generators: make(map[string]GeneratorInfo),
}
}
// Register adds a generator to the registry
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,
}
}
// GetAll returns all registered generators
func (m *GeneratorManager) GetAll() map[string]GeneratorInfo {
return m.generators
}
// GetCommands returns cobra commands for all registered generators
func (m *GeneratorManager) GetCommands() []*cobra.Command {
var commands []*cobra.Command
for _, info := range m.generators {
commands = append(commands, info.Creator())
}
return commands
}
// PrintGeneratorsTable prints a table of all available generators with their stability
func (m *GeneratorManager) PrintGeneratorsTable() error {
tableData := [][]string{
{"Generator", "Description", "Stability"},
}
// Get generator names for consistent ordering
var names []string
for name := range m.generators {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
info := m.generators[name]
tableData = append(tableData, []string{
name,
info.Description,
string(info.Stability),
})
}
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
}
// DefaultManager is the default instance of the generator manager
var DefaultManager = NewGeneratorManager()

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

@ -0,0 +1,66 @@
package nodejs
import (
_ "embed"
"encoding/json"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type NodejsGenerator struct {
generators.CommonGenerator
}
type Params struct {
}
//go:embed nodejs.tmpl
var nodejsTmpl 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 *NodejsGenerator) 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, nodejsTmpl, newParams, "openfeature.ts")
}
// NewGenerator creates a generator for NodeJS.
func NewGenerator(fs *flagset.Flagset) *NodejsGenerator {
return &NodejsGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,87 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
import {
OpenFeature,
stringOrUndefined,
objectOrUndefined,
JsonValue,
} from "@openfeature/server-sdk";
import type {
EvaluationContext,
EvaluationDetails,
FlagEvaluationOptions,
} from "@openfeature/server-sdk";
export interface GeneratedClient {
{{- range .Flagset.Flags }}
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - 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<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>} Flag evaluation response
*/
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>;
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - 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<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>} Flag evaluation details response
*/
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>;
{{ end -}}
}
/**
* A factory function that returns a generated client that not bound to a domain.
* It was generated using the OpenFeature CLI and is compatible with `@openfeature/server-sdk`.
*
* All domainless or unbound clients use the default provider set via {@link OpenFeature.setProvider}.
* @param {EvaluationContext} context Evaluation context that should be set on the client to used during flag evaluations
* @returns {GeneratedClient} Generated OpenFeature Client
*/
export function getGeneratedClient(context?: EvaluationContext): GeneratedClient
/**
* A factory function that returns a domain-bound generated client that was
* created using the OpenFeature CLI and is compatible with the `@openfeature/server-sdk`.
*
* If there is already a provider bound to this domain via {@link OpenFeature.setProvider}, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
* @param {string} domain An identifier which logically binds clients with providers
* @param {EvaluationContext} context Evaluation context that should be set on the client to used during flag evaluations
* @returns {GeneratedClient} Generated OpenFeature Client
*/
export function getGeneratedClient(domain: string, context?: EvaluationContext): GeneratedClient
export function getGeneratedClient(domainOrContext?: string | EvaluationContext, contextOrUndefined?: EvaluationContext): GeneratedClient {
const domain = stringOrUndefined(domainOrContext);
const context =
objectOrUndefined<EvaluationContext>(domainOrContext) ??
objectOrUndefined<EvaluationContext>(contextOrUndefined);
const client = domain ? OpenFeature.getClient(domain, context) : OpenFeature.getClient(context)
return {
{{- range .Flagset.Flags }}
{{ .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<{{ 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

@ -0,0 +1,66 @@
package react
import (
_ "embed"
"encoding/json"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type ReactGenerator struct {
generators.CommonGenerator
}
type Params struct {
}
//go:embed react.tmpl
var reactTmpl 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 *ReactGenerator) 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, reactTmpl, newParams, "openfeature.ts")
}
// NewGenerator creates a generator for React.
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
return &ReactGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -0,0 +1,37 @@
'use client';
import {
type ReactFlagEvaluationOptions,
type ReactFlagEvaluationNoSuspenseOptions,
useFlag,
useSuspenseFlag,
JsonValue
} from "@openfeature/react-sdk";
{{ range .Flagset.Flags }}
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - 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 }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
};
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - 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 }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
};
{{ end}}

117
internal/logger/logger.go Normal file
View File

@ -0,0 +1,117 @@
package logger
import (
"path/filepath"
"github.com/pterm/pterm"
)
// Logger provides methods for logging different types of messages
type Logger interface {
// Println logs a message without logging level
Println(message string)
// Info logs general information
Info(message string)
// Success logs successful operations
Success(message string)
// Warning logs warnings
Warning(message string)
// Error logs errors
Error(message string)
// Debug logs debug information (only when debug mode is enabled)
Debug(message string)
// SetDebug enables or disables debug mode
SetDebug(enabled bool)
// IsDebugEnabled returns whether debug mode is enabled
IsDebugEnabled() bool
// FileCreated logs a file creation event
FileCreated(path string)
// FileFailed logs a file creation failure
FileFailed(path string, err error)
// GenerationStarted logs the start of a generation process
GenerationStarted(generatorType string)
// GenerationComplete logs the completion of a generation process
GenerationComplete(generatorType string)
}
// DefaultLogger is the default implementation of Logger
type DefaultLogger struct {
debugEnabled bool
}
// New creates a new DefaultLogger
func New() *DefaultLogger {
return &DefaultLogger{
debugEnabled: false,
}
}
// SetDebug enables or disables debug mode
func (l *DefaultLogger) SetDebug(enabled bool) {
l.debugEnabled = enabled
if enabled {
pterm.EnableDebugMessages()
}
}
// IsDebugEnabled returns whether debug mode is enabled
func (l *DefaultLogger) IsDebugEnabled() bool {
return l.debugEnabled
}
// Println logs a message without logging level
func (l *DefaultLogger) Println(message string) {
pterm.Println(message)
}
// Info logs general information
func (l *DefaultLogger) Info(message string) {
pterm.Info.Println(message)
}
// Success logs successful operations
func (l *DefaultLogger) Success(message string) {
pterm.Success.Println(message)
}
// Warning logs warnings
func (l *DefaultLogger) Warning(message string) {
pterm.Warning.Println(message)
}
// Error logs errors
func (l *DefaultLogger) Error(message string) {
pterm.Error.Println(message)
}
// Debug logs debug information (only when debug mode is enabled)
func (l *DefaultLogger) Debug(message string) {
if l.debugEnabled {
pterm.Debug.Println(message)
}
}
// FileCreated logs a file creation event
func (l *DefaultLogger) FileCreated(path string) {
prettyPath := pterm.LightWhite(filepath.Clean(path))
pterm.Success.Printf("Created %s\n", prettyPath)
}
// FileFailed logs a file creation failure
func (l *DefaultLogger) FileFailed(path string, err error) {
prettyPath := pterm.LightWhite(filepath.Clean(path))
pterm.Error.Printf("Failed to create %s: %v\n", prettyPath, err)
}
// GenerationStarted logs the start of a generation process
func (l *DefaultLogger) GenerationStarted(generatorType string) {
pterm.Info.Printf("Generating a typesafe client for %s\n", generatorType)
}
// GenerationComplete logs the completion of a generation process
func (l *DefaultLogger) GenerationComplete(generatorType string) {
pterm.Success.Printf("Successfully generated client. Happy coding!\n")
}
// Default is a singleton instance of DefaultLogger
var Default Logger = New()

View File

@ -0,0 +1,50 @@
package manifest
import (
"fmt"
"reflect"
)
type Change struct {
Type string `json:"type"`
Path string `json:"path"`
OldValue any `json:"oldValue,omitempty"`
NewValue any `json:"newValue,omitempty"`
}
func Compare(oldManifest, newManifest *Manifest) ([]Change, error) {
var changes []Change
oldFlags := oldManifest.Flags
newFlags := newManifest.Flags
for key, newFlag := range newFlags {
if oldFlag, exists := oldFlags[key]; exists {
if !reflect.DeepEqual(oldFlag, newFlag) {
changes = append(changes, Change{
Type: "change",
Path: fmt.Sprintf("flags.%s", key),
OldValue: oldFlag,
NewValue: newFlag,
})
}
} else {
changes = append(changes, Change{
Type: "add",
Path: fmt.Sprintf("flags.%s", key),
NewValue: newFlag,
})
}
}
for key, oldFlag := range oldFlags {
if _, exists := newFlags[key]; !exists {
changes = append(changes, Change{
Type: "remove",
Path: fmt.Sprintf("flags.%s", key),
OldValue: oldFlag,
})
}
}
return changes, nil
}

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

@ -0,0 +1,124 @@
package manifest
import (
"log"
"github.com/invopop/jsonschema"
"github.com/pterm/pterm"
)
type BooleanFlag struct {
BaseFlag
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"enum=boolean"`
// The value returned from an unsuccessful flag evaluation
DefaultValue bool `json:"defaultValue,omitempty"`
}
type StringFlag struct {
BaseFlag
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"enum=string"`
// The value returned from an unsuccessful flag evaluation
DefaultValue string `json:"defaultValue,omitempty"`
}
type IntegerFlag struct {
BaseFlag
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"enum=integer"`
// The value returned from an unsuccessful flag evaluation
DefaultValue int `json:"defaultValue,omitempty"`
}
type FloatFlag struct {
BaseFlag
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"enum=float"`
// The value returned from an unsuccessful flag evaluation
DefaultValue float64 `json:"defaultValue,omitempty"`
}
type ObjectFlag struct {
BaseFlag
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"enum=object"`
// The value returned from an unsuccessful flag evaluation
DefaultValue any `json:"defaultValue,omitempty"`
}
type BaseFlag struct {
// The type of feature flag (e.g., boolean, string, integer, float)
Type string `json:"flagType,omitempty" jsonschema:"required"`
// A concise description of this feature flag's purpose.
Description string `json:"description,omitempty"`
}
// Feature flag manifest for the OpenFeature CLI
type Manifest struct {
// Collection of feature flag definitions
Flags map[string]any `json:"flags" jsonschema:"title=Flags,required"`
}
// Converts the Manifest struct to a JSON schema.
func ToJSONSchema() *jsonschema.Schema {
reflector := &jsonschema.Reflector{
ExpandedStruct: true,
AllowAdditionalProperties: true,
BaseSchemaID: "openfeature-cli",
}
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
pterm.Error.Printf("Error extracting comments from types.go: %v\n", err)
}
schema := reflector.Reflect(Manifest{})
schema.Version = "http://json-schema.org/draft-07/schema#"
schema.Title = "OpenFeature CLI Manifest"
flags, ok := schema.Properties.Get("flags")
if !ok {
log.Fatal("flags not found")
}
flags.PatternProperties = map[string]*jsonschema.Schema{
"^.{1,}$": {
Ref: "#/$defs/flag",
},
}
// We only want flags keys that matches the pattern properties
flags.AdditionalProperties = jsonschema.FalseSchema
schema.Definitions = jsonschema.Definitions{
"flag": &jsonschema.Schema{
OneOf: []*jsonschema.Schema{
{Ref: "#/$defs/booleanFlag"},
{Ref: "#/$defs/stringFlag"},
{Ref: "#/$defs/integerFlag"},
{Ref: "#/$defs/floatFlag"},
{Ref: "#/$defs/objectFlag"},
},
Required: []string{"flagType", "defaultValue"},
},
"booleanFlag": &jsonschema.Schema{
Type: "object",
Properties: reflector.Reflect(BooleanFlag{}).Properties,
},
"stringFlag": &jsonschema.Schema{
Type: "object",
Properties: reflector.Reflect(StringFlag{}).Properties,
},
"integerFlag": &jsonschema.Schema{
Type: "object",
Properties: reflector.Reflect(IntegerFlag{}).Properties,
},
"floatFlag": &jsonschema.Schema{
Type: "object",
Properties: reflector.Reflect(FloatFlag{}).Properties,
},
"objectFlag": &jsonschema.Schema{
Type: "object",
Properties: reflector.Reflect(ObjectFlag{}).Properties,
},
}
return schema
}

View File

@ -0,0 +1,44 @@
package manifest
import (
"encoding/json"
"github.com/open-feature/cli/internal/filesystem"
"github.com/spf13/afero"
)
type initManifest struct {
Schema string `json:"$schema,omitempty"`
Manifest
}
// 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/main/schema/v0/flag-manifest.json",
Manifest: Manifest{
Flags: map[string]any{},
},
}
formattedInitManifest, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
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),
}
}

View File

@ -0,0 +1,44 @@
package manifest
import (
"fmt"
"strings"
"github.com/open-feature/cli/schema/v0"
"github.com/xeipuuv/gojsonschema"
)
type ValidationError struct {
Type string `json:"type"`
Path string `json:"path"`
Message string `json:"message"`
}
func Validate(data []byte) ([]ValidationError, error) {
schemaLoader := gojsonschema.NewStringLoader(schema.SchemaFile)
manifestLoader := gojsonschema.NewBytesLoader(data)
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
if err != nil {
return nil, fmt.Errorf("failed to validate manifest: %w", err)
}
var issues []ValidationError
for _, err := range result.Errors() {
if strings.HasPrefix(err.Field(), "flags") && err.Type() == "number_one_of" {
issues = append(issues, ValidationError{
Type: err.Type(),
Path: err.Field(),
Message: "flagType must be 'boolean', 'string', 'integer', 'float', or 'object'",
})
} else {
issues = append(issues, ValidationError{
Type: err.Type(),
Path: err.Field(),
Message: err.Description(),
})
}
}
return issues, nil
}

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

@ -1,4 +1,5 @@
{
"$schema": "../schema/v0/flag-manifest.json",
"flags": {
"enableFeatureA": {
"flagType": "boolean",

36
schema/generate-schema.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/open-feature/cli/internal/manifest"
)
const schemaPath = "schema/v0/flag-manifest.json"
func main() {
schema := manifest.ToJSONSchema()
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
log.Fatal(fmt.Errorf("failed to marshal JSON schema: %w", err))
}
if err := os.MkdirAll("schema/v0", os.ModePerm); err != nil {
log.Fatal(fmt.Errorf("failed to create directory: %w", err))
}
file, err := os.Create(schemaPath)
if err != nil {
log.Fatal(fmt.Errorf("failed to create file: %w", err))
}
defer file.Close()
if _, err := file.Write(data); err != nil {
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err))
}
fmt.Println("JSON schema generated successfully at " + schemaPath)
}

View File

@ -0,0 +1,147 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "openfeature-cli/manifest",
"$defs": {
"booleanFlag": {
"properties": {
"flagType": {
"type": "string",
"enum": [
"boolean"
],
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
},
"description": {
"type": "string",
"description": "A concise description of this feature flag's purpose."
},
"defaultValue": {
"type": "boolean",
"description": "The value returned from an unsuccessful flag evaluation"
}
},
"type": "object"
},
"flag": {
"oneOf": [
{
"$ref": "#/$defs/booleanFlag"
},
{
"$ref": "#/$defs/stringFlag"
},
{
"$ref": "#/$defs/integerFlag"
},
{
"$ref": "#/$defs/floatFlag"
},
{
"$ref": "#/$defs/objectFlag"
}
],
"required": [
"flagType",
"defaultValue"
]
},
"floatFlag": {
"properties": {
"flagType": {
"type": "string",
"enum": [
"float"
],
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
},
"description": {
"type": "string",
"description": "A concise description of this feature flag's purpose."
},
"defaultValue": {
"type": "number",
"description": "The value returned from an unsuccessful flag evaluation"
}
},
"type": "object"
},
"integerFlag": {
"properties": {
"flagType": {
"type": "string",
"enum": [
"integer"
],
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
},
"description": {
"type": "string",
"description": "A concise description of this feature flag's purpose."
},
"defaultValue": {
"type": "integer",
"description": "The value returned from an unsuccessful flag evaluation"
}
},
"type": "object"
},
"objectFlag": {
"properties": {
"flagType": {
"type": "string",
"enum": [
"object"
],
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
},
"description": {
"type": "string",
"description": "A concise description of this feature flag's purpose."
},
"defaultValue": {
"description": "The value returned from an unsuccessful flag evaluation"
}
},
"type": "object"
},
"stringFlag": {
"properties": {
"flagType": {
"type": "string",
"enum": [
"string"
],
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
},
"description": {
"type": "string",
"description": "A concise description of this feature flag's purpose."
},
"defaultValue": {
"type": "string",
"description": "The value returned from an unsuccessful flag evaluation"
}
},
"type": "object"
}
},
"properties": {
"flags": {
"patternProperties": {
"^.{1,}$": {
"$ref": "#/$defs/flag"
}
},
"additionalProperties": false,
"type": "object",
"title": "Flags",
"description": "Collection of feature flag definitions"
}
},
"type": "object",
"required": [
"flags"
],
"title": "OpenFeature CLI Manifest",
"description": "Feature flag manifest for the OpenFeature CLI"
}

View File

@ -1,135 +0,0 @@
{
"$id": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Flag Manifest",
"description": "Describes a configuration of OpenFeature flags, including info such as their types and default values.",
"type": "object",
"properties": {
"flags": {
"description": "Object containing the flags in the config",
"type": "object",
"patternProperties": {
"^.{1,}$": {
"description": "The definition of one flag",
"$ref": "#/$defs/flag"
}
},
"additionalProperties": false,
"minProperties": 1
}
},
"required": [
"flags"
],
"$defs": {
"flag": {
"oneOf": [
{
"$ref": "#/$defs/booleanType"
},
{
"$ref": "#/$defs/stringType"
},
{
"$ref": "#/$defs/integerType"
},
{
"$ref": "#/$defs/floatType"
},
{
"$ref": "#/$defs/objectType"
}
],
"required": [
"flagType",
"defaultValue"
]
},
"booleanType": {
"type": "object",
"properties": {
"flagType": {
"type": "string",
"enum": [
"boolean"
]
},
"defaultValue": {
"description": "The default value returned in code if a flag evaluation is unsuccessful",
"type": "boolean"
},
"description": {
"type": "string"
}
}
},
"stringType": {
"type": "object",
"properties": {
"flagType": {
"type": "string",
"enum": [
"string"
]
},
"defaultValue": {
"type": "string"
},
"description": {
"type": "string"
}
}
},
"integerType": {
"type": "object",
"properties": {
"flagType": {
"type": "string",
"enum": [
"integer"
]
},
"defaultValue": {
"type": "integer"
},
"description": {
"type": "string"
}
}
},
"floatType": {
"type": "object",
"properties": {
"flagType": {
"type": "string",
"enum": [
"float"
]
},
"defaultValue": {
"type": "number"
},
"description": {
"type": "string"
}
}
},
"objectType": {
"type": "object",
"properties": {
"flagType": {
"type": "string",
"enum": [
"object"
]
},
"defaultValue": {
"type": "object"
},
"description": {
"type": "string"
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More