mirror of https://github.com/open-feature/cli.git
Compare commits
48 Commits
Author | SHA1 | Date |
---|---|---|
|
2003666eaa | |
|
cf962d967c | |
|
2835e3cf10 | |
|
288023c5dd | |
|
b867485101 | |
|
6145c2e15d | |
|
32f912a089 | |
|
594cf538be | |
|
5d7a60754a | |
|
8b70612472 | |
|
51232fea1c | |
|
4f909c1068 | |
|
1b4d062045 | |
|
35533dceb0 | |
|
a25f90a65b | |
|
11b9c34d86 | |
|
3ec8443823 | |
|
83dac705b9 | |
|
bcd11ea9c8 | |
|
79d3dceb3a | |
|
8eec77965a | |
|
eedccd606f | |
|
49e65c8283 | |
|
c1466b48e2 | |
|
063cfca2d7 | |
|
9a9f11fc6c | |
|
96f4cde0f8 | |
|
230956e6b4 | |
|
5210429e39 | |
|
e32547f734 | |
|
ae645813c4 | |
|
1f8f43ae04 | |
|
412a1174b5 | |
|
fa82a179e1 | |
|
3fbd94726d | |
|
a40b6a4d31 | |
|
9244276fc4 | |
|
afa46d00b3 | |
|
9102d1390a | |
|
05e094db68 | |
|
0f4ba1f5a2 | |
|
0e28e8ec3b | |
|
e988d75996 | |
|
fdfe561d49 | |
|
67f45c1e28 | |
|
34afca62ab | |
|
6c36ee90f7 | |
|
106bf9ddfe |
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -24,3 +24,13 @@ go.work.sum
|
|||
# env file
|
||||
.env
|
||||
dist
|
||||
|
||||
# openfeature cli config
|
||||
.openfeature.yaml
|
||||
|
||||
.idea/
|
||||
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
generated/
|
||||
*.log
|
|
@ -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="OpenFeature’s 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="OpenFeature’s 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="OpenFeature’s 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="OpenFeature’s 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
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
".": "0.1.10"
|
||||
".": "0.3.5"
|
||||
}
|
||||
|
|
86
CHANGELOG.md
86
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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"]
|
||||
|
|
39
Makefile
39
Makefile
|
@ -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
216
README.md
|
@ -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).
|
||||
|
|
|
@ -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
|
23
cmd/docs.go
23
cmd/docs.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
|
@ -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
|
41
cmd/root.go
41
cmd/root.go
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -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
99
go.mod
|
@ -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
294
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
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(¶ms)
|
||||
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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/open-feature/cli/main/schema/v0/flag-manifest.json",
|
||||
"flags": {}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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);
|
||||
};
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -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}}
|
|
@ -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
|
||||
}
|
|
@ -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!
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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 -}}
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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 " " }}}
|
||||
}
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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{}),
|
||||
}
|
||||
}
|
|
@ -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}}
|
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>open-feature/community-tooling"]
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema/v0/flag-manifest.json",
|
||||
"flags": {
|
||||
"enableFeatureA": {
|
||||
"flagType": "boolean",
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue