mirror of https://github.com/open-feature/cli.git
Compare commits
33 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 |
|
@ -4,6 +4,7 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
merge_group:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
# Required: allow read access to the content for analysis.
|
# Required: allow read access to the content for analysis.
|
||||||
|
|
|
@ -44,3 +44,22 @@ jobs:
|
||||||
git diff
|
git diff
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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'
|
||||||
|
|
|
@ -27,3 +27,10 @@ dist
|
||||||
|
|
||||||
# openfeature cli config
|
# openfeature cli config
|
||||||
.openfeature.yaml
|
.openfeature.yaml
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
generated/
|
||||||
|
*.log
|
|
@ -21,6 +21,7 @@ builds:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
- darwin
|
- darwin
|
||||||
|
binary: ./cmd/openfeature
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- formats: tar.gz
|
- formats: tar.gz
|
||||||
|
@ -35,49 +36,49 @@ archives:
|
||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats: [ 'zip' ]
|
formats: ["zip"]
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
report_sizes: true
|
report_sizes: true
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
|
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
use: buildx
|
use: buildx
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- --platform=linux/amd64
|
- --platform=linux/amd64
|
||||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/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.source=https://github.com/open-feature/cli
|
||||||
- --label=org.opencontainers.image.version={{ .Version }}
|
- --label=org.opencontainers.image.version={{ .Version }}
|
||||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
- --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.description="OpenFeature’s official command-line tool"
|
||||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||||
|
|
||||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
|
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
use: buildx
|
use: buildx
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- --platform=linux/arm64
|
- --platform=linux/arm64
|
||||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/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.source=https://github.com/open-feature/cli
|
||||||
- --label=org.opencontainers.image.version={{ .Version }}
|
- --label=org.opencontainers.image.version={{ .Version }}
|
||||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
- --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.description="OpenFeature’s official command-line tool"
|
||||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||||
|
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
|
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||||
- name_template: ghcr.io/open-feature/cli:latest
|
- name_template: ghcr.io/open-feature/cli:latest
|
||||||
image_templates:
|
image_templates:
|
||||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
".": "0.3.2"
|
".": "0.3.5"
|
||||||
}
|
}
|
||||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,5 +1,40 @@
|
||||||
# Changelog
|
# 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)
|
## [0.3.2](https://github.com/open-feature/cli/compare/v0.3.1...v0.3.2) (2025-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
# Contributing to OpenFeature CLI
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to the OpenFeature CLI! This document provides guidelines and instructions to help you get started with contributing to the project. Whether you're fixing a bug, adding a new feature, or improving documentation, your contributions are greatly appreciated.
|
||||||
|
|
||||||
## Contributing New Generators
|
## 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:
|
We welcome contributions for new generators to extend the functionality of the OpenFeature CLI. Below are the steps to contribute a new generator:
|
||||||
|
@ -32,6 +36,81 @@ We welcome contributions for new generators to extend the functionality of the O
|
||||||
|
|
||||||
11. **Address Feedback**: Be responsive to feedback from the maintainers. Make any necessary changes and update your pull request as needed.
|
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
|
## Templates
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM alpine:3.21
|
FROM alpine:3.22
|
||||||
|
|
||||||
COPY ./openfeature usr/local/bin/openfeature
|
COPY ./openfeature usr/local/bin/openfeature
|
||||||
|
|
||||||
|
|
28
Makefile
28
Makefile
|
@ -1,10 +1,30 @@
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
@echo "Running tests..."
|
@echo "Running tests..."
|
||||||
@go test -v ./...
|
@go test -v ./...
|
||||||
@echo "Tests passed successfully!"
|
@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:
|
generate-docs:
|
||||||
@echo "Generating documentation..."
|
@echo "Generating documentation..."
|
||||||
@go run ./docs/generate-commands.go
|
@go run ./docs/generate-commands.go
|
||||||
|
@ -14,3 +34,9 @@ generate-schema:
|
||||||
@echo "Generating schema..."
|
@echo "Generating schema..."
|
||||||
@go run ./schema/generate-schema.go
|
@go run ./schema/generate-schema.go
|
||||||
@echo "Schema generated successfully!"
|
@echo "Schema generated successfully!"
|
||||||
|
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt:
|
||||||
|
@echo "Running go fmt..."
|
||||||
|
@go fmt ./...
|
||||||
|
@echo "Code formatted successfully!"
|
|
@ -58,7 +58,7 @@ docker run -it -v $(pwd):/local -w /local ghcr.io/open-feature/cli:latest
|
||||||
If you have `Go >= 1.23` installed, you can install the CLI using the following command:
|
If you have `Go >= 1.23` installed, you can install the CLI using the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/open-feature/cli@latest
|
go install github.com/open-feature/cli/cmd/openfeature@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### via pre-built binaries
|
### via pre-built binaries
|
||||||
|
|
209
cmd/generate.go
209
cmd/generate.go
|
@ -1,209 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/config"
|
|
||||||
"github.com/open-feature/cli/internal/flagset"
|
|
||||||
"github.com/open-feature/cli/internal/generators"
|
|
||||||
"github.com/open-feature/cli/internal/generators/golang"
|
|
||||||
"github.com/open-feature/cli/internal/generators/nodejs"
|
|
||||||
"github.com/open-feature/cli/internal/generators/react"
|
|
||||||
"github.com/open-feature/cli/internal/logger"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// addStabilityInfo adds stability information to the command's help template before "Usage:"
|
|
||||||
func addStabilityInfo(cmd *cobra.Command) {
|
|
||||||
// Only modify commands that have a stability annotation
|
|
||||||
if stability, ok := cmd.Annotations["stability"]; ok {
|
|
||||||
originalTemplate := cmd.UsageTemplate()
|
|
||||||
|
|
||||||
// Find the "Usage:" section and insert stability info before it
|
|
||||||
if strings.Contains(originalTemplate, "Usage:") {
|
|
||||||
customTemplate := strings.Replace(
|
|
||||||
originalTemplate,
|
|
||||||
"Usage:",
|
|
||||||
"Stability: "+stability+"\n\nUsage:",
|
|
||||||
1, // Replace only the first occurrence
|
|
||||||
)
|
|
||||||
cmd.SetUsageTemplate(customTemplate)
|
|
||||||
} else {
|
|
||||||
// Fallback if "Usage:" not found - prepend to the template
|
|
||||||
customTemplate := "Stability: " + stability + "\n\n" + originalTemplate
|
|
||||||
cmd.SetUsageTemplate(customTemplate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGenerateNodeJSCmd() *cobra.Command {
|
|
||||||
nodeJSCmd := &cobra.Command{
|
|
||||||
Use: "nodejs",
|
|
||||||
Short: "Generate typesafe Node.js client.",
|
|
||||||
Long: `Generate typesafe Node.js client compatible with the OpenFeature JavaScript Server SDK.`,
|
|
||||||
Annotations: map[string]string{
|
|
||||||
"stability": string(generators.Alpha),
|
|
||||||
},
|
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return initializeConfig(cmd, "generate.nodejs")
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
manifestPath := config.GetManifestPath(cmd)
|
|
||||||
outputPath := config.GetOutputPath(cmd)
|
|
||||||
|
|
||||||
logger.Default.GenerationStarted("Node.js")
|
|
||||||
|
|
||||||
params := generators.Params[nodejs.Params]{
|
|
||||||
OutputPath: outputPath,
|
|
||||||
Custom: nodejs.Params{},
|
|
||||||
}
|
|
||||||
flagset, err := flagset.Load(manifestPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
generator := nodejs.NewGenerator(flagset)
|
|
||||||
logger.Default.Debug("Executing Node.js generator")
|
|
||||||
err = generator.Generate(¶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 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 init() {
|
|
||||||
// Register generators with the manager
|
|
||||||
generators.DefaultManager.Register(GetGenerateReactCmd)
|
|
||||||
generators.DefaultManager.Register(GetGenerateGoCmd)
|
|
||||||
generators.DefaultManager.Register(GetGenerateNodeJSCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGenerateCmd() *cobra.Command {
|
|
||||||
generateCmd := &cobra.Command{
|
|
||||||
Use: "generate",
|
|
||||||
Short: "Generate typesafe OpenFeature accessors.",
|
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return initializeConfig(cmd, "generate")
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
cmd.Println("Available generators:")
|
|
||||||
return generators.DefaultManager.PrintGeneratorsTable()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add generate flags using the config package
|
|
||||||
config.AddGenerateFlags(generateCmd)
|
|
||||||
|
|
||||||
// Add all registered generator commands
|
|
||||||
for _, subCmd := range generators.DefaultManager.GetCommands() {
|
|
||||||
generateCmd.AddCommand(subCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
addStabilityInfo(generateCmd)
|
|
||||||
|
|
||||||
return generateCmd
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/open-feature/cli/cmd"
|
import "github.com/open-feature/cli/internal/cmd"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Overridden by Go Releaser at build time
|
// Overridden by Go Releaser at build time
|
|
@ -23,6 +23,7 @@ openfeature [flags]
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [openfeature compare](openfeature_compare.md) - Compare two feature flag manifests
|
||||||
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
|
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
|
||||||
* [openfeature init](openfeature_init.md) - Initialize a new project
|
* [openfeature init](openfeature_init.md) - Initialize a new project
|
||||||
* [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI
|
* [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.
|
||||||
|
|
|
@ -26,7 +26,11 @@ openfeature generate [flags]
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
|
||||||
* [openfeature](openfeature.md) - CLI for OpenFeature.
|
* [openfeature](openfeature.md) - CLI for OpenFeature.
|
||||||
|
* [openfeature generate csharp](openfeature_generate_csharp.md) - Generate typesafe C# client.
|
||||||
* [openfeature generate go](openfeature_generate_go.md) - Generate typesafe accessors for OpenFeature.
|
* [openfeature generate 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 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.
|
* [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.
|
||||||
|
|
|
@ -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 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.
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/open-feature/cli/cmd"
|
"github.com/open-feature/cli/internal/cmd"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
)
|
)
|
||||||
|
|
70
go.mod
70
go.mod
|
@ -2,53 +2,85 @@ module github.com/open-feature/cli
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.24.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.6.0
|
dagger.io/dagger v0.18.12
|
||||||
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/iancoleman/strcase v0.3.0
|
github.com/iancoleman/strcase v0.3.0
|
||||||
github.com/invopop/jsonschema v0.13.0
|
github.com/invopop/jsonschema v0.13.0
|
||||||
github.com/pterm/pterm v0.12.80
|
github.com/pterm/pterm v0.12.81
|
||||||
github.com/spf13/afero v1.14.0
|
github.com/spf13/afero v1.14.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/pflag v1.0.6
|
github.com/spf13/pflag v1.0.6
|
||||||
github.com/spf13/viper v1.20.0
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
github.com/xeipuuv/gojsonschema v1.2.0
|
||||||
golang.org/x/text v0.23.0
|
golang.org/x/text v0.26.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
atomicgo.dev/cursor v0.2.0 // indirect
|
atomicgo.dev/cursor v0.2.0 // indirect
|
||||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||||
atomicgo.dev/schedule v0.1.0 // 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/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/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/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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.8.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.28 // indirect
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/term v0.30.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/term v0.32.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||||
|
google.golang.org/grpc v1.73.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
)
|
)
|
||||||
|
|
161
go.sum
161
go.sum
|
@ -6,6 +6,18 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||||
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
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.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.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.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||||
|
@ -15,38 +27,56 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
|
||||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
github.com/MarvinJWendt/testza v0.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 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
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/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 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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.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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
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 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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.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.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
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 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.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.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
@ -61,13 +91,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
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/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -78,32 +110,34 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
|
||||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
github.com/pterm/pterm v0.12.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.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||||
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
|
github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
|
||||||
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
|
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
|
||||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
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 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
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/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
@ -113,10 +147,15 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8xLRkY=
|
||||||
|
github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-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 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
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 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
@ -125,21 +164,61 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
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=
|
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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
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-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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 h1:gllJVKwONftmCc4KlNbN8o/LvmbxotqQy6zzi6yDQOQ=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
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.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/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-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-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.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.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-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.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.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-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-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -149,31 +228,41 @@ golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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-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-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-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.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
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.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.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.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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
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=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/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-20200313102051-9f266ea9e77c/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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,6 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
||||||
logger.Default.Debug(fmt.Sprintf("Using config file: %s", v.ConfigFileUsed()))
|
logger.Default.Debug(fmt.Sprintf("Using config file: %s", v.ConfigFileUsed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Track which flags were set directly via command line
|
// Track which flags were set directly via command line
|
||||||
cmdLineFlags := make(map[string]bool)
|
cmdLineFlags := make(map[string]bool)
|
||||||
cmd.Flags().Visit(func(f *pflag.Flag) {
|
cmd.Flags().Visit(func(f *pflag.Flag) {
|
||||||
|
@ -53,7 +52,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
||||||
|
|
||||||
// Check the most specific path (e.g., generate.go.package-name)
|
// Check the most specific path (e.g., generate.go.package-name)
|
||||||
if bindPrefix != "" {
|
if bindPrefix != "" {
|
||||||
configPaths = append(configPaths, bindPrefix + "." + f.Name)
|
configPaths = append(configPaths, bindPrefix+"."+f.Name)
|
||||||
|
|
||||||
// Check parent paths (e.g., generate.package-name)
|
// Check parent paths (e.g., generate.package-name)
|
||||||
parts := strings.Split(bindPrefix, ".")
|
parts := strings.Split(bindPrefix, ".")
|
|
@ -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)
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ type generateTestCase struct {
|
||||||
outputGolden string // path to the golden output file
|
outputGolden string // path to the golden output file
|
||||||
outputPath string // output directory (optional, defaults to "output")
|
outputPath string // output directory (optional, defaults to "output")
|
||||||
outputFile string // output file name
|
outputFile string // output file name
|
||||||
packageName string // optional, only used for Go
|
packageName string // optional, used for Go (package-name), Java (package-name) and C# (namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerate(t *testing.T) {
|
func TestGenerate(t *testing.T) {
|
||||||
|
@ -48,6 +48,36 @@ func TestGenerate(t *testing.T) {
|
||||||
outputGolden: "testdata/success_nodejs.golden",
|
outputGolden: "testdata/success_nodejs.golden",
|
||||||
outputFile: "openfeature.ts",
|
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
|
// Add more test cases here as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +109,15 @@ func TestGenerate(t *testing.T) {
|
||||||
"--output", outputPath,
|
"--output", outputPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add package name if provided (for Go)
|
// Add parameters specific to each generator
|
||||||
if tc.packageName != "" {
|
if tc.packageName != "" {
|
||||||
args = append(args, "--package-name", tc.packageName)
|
if tc.command == "csharp" {
|
||||||
|
args = append(args, "--namespace", tc.packageName)
|
||||||
|
} else if tc.command == "go" {
|
||||||
|
args = append(args, "--package-name", tc.packageName)
|
||||||
|
} else if tc.command == "java" {
|
||||||
|
args = append(args, "--package-name", tc.packageName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.SetArgs(args)
|
cmd.SetArgs(args)
|
||||||
|
@ -100,7 +136,7 @@ func TestGenerate(t *testing.T) {
|
||||||
|
|
||||||
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
|
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
|
||||||
data, err := os.ReadFile(inputPath)
|
data, err := os.ReadFile(inputPath)
|
||||||
if (err != nil) {
|
if err != nil {
|
||||||
t.Fatalf("error reading file %q: %v", inputPath, err)
|
t.Fatalf("error reading file %q: %v", inputPath, err)
|
||||||
}
|
}
|
||||||
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
|
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
|
||||||
|
@ -120,6 +156,17 @@ func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeLines trims trailing whitespace and carriage returns from each line.
|
||||||
|
// This helps ensure consistent comparison by ignoring formatting differences like indentation or line endings.
|
||||||
|
func normalizeLines(input []string) []string {
|
||||||
|
normalized := make([]string, len(input))
|
||||||
|
for i, line := range input {
|
||||||
|
// Trim right whitespace and convert \r\n or \r to \n
|
||||||
|
normalized[i] = strings.TrimRight(line, " \t\r")
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
|
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
|
||||||
want, err := os.ReadFile(testFile)
|
want, err := os.ReadFile(testFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -132,8 +179,8 @@ func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to string arrays by splitting on newlines
|
// Convert to string arrays by splitting on newlines
|
||||||
wantLines := strings.Split(string(want), "\n")
|
wantLines := normalizeLines(strings.Split(string(want), "\n"))
|
||||||
gotLines := strings.Split(string(got), "\n")
|
gotLines := normalizeLines(strings.Split(string(got), "\n"))
|
||||||
|
|
||||||
if diff := cmp.Diff(wantLines, gotLines); diff != "" {
|
if diff := cmp.Diff(wantLines, gotLines); diff != "" {
|
||||||
t.Errorf("output mismatch (-want +got):\n%s", diff)
|
t.Errorf("output mismatch (-want +got):\n%s", diff)
|
|
@ -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)
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ func GetRootCmd() *cobra.Command {
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
printBanner()
|
printBanner()
|
||||||
logger.Default.Println("");
|
logger.Default.Println("")
|
||||||
logger.Default.Println("To see all the options, try 'openfeature --help'")
|
logger.Default.Println("To see all the options, try 'openfeature --help'")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -62,6 +62,7 @@ func GetRootCmd() *cobra.Command {
|
||||||
rootCmd.AddCommand(GetVersionCmd())
|
rootCmd.AddCommand(GetVersionCmd())
|
||||||
rootCmd.AddCommand(GetInitCmd())
|
rootCmd.AddCommand(GetInitCmd())
|
||||||
rootCmd.AddCommand(GetGenerateCmd())
|
rootCmd.AddCommand(GetGenerateCmd())
|
||||||
|
rootCmd.AddCommand(GetCompareCmd())
|
||||||
|
|
||||||
// Add a custom error handler after the command is created
|
// Add a custom error handler after the command is created
|
||||||
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
|
||||||
type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, 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 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
|
var client openfeature.IClient = nil
|
||||||
// Discount percentage applied to purchases.
|
// Discount percentage applied to purchases.
|
||||||
|
@ -67,6 +69,23 @@ var GreetingMessage = struct {
|
||||||
return client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx)
|
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.
|
// Maximum allowed length for usernames.
|
||||||
var UsernameMaxLength = struct {
|
var UsernameMaxLength = struct {
|
||||||
// Value returns the value of the flag UsernameMaxLength,
|
// Value returns the value of the flag UsernameMaxLength,
|
|
@ -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 });
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import {
|
||||||
OpenFeature,
|
OpenFeature,
|
||||||
stringOrUndefined,
|
stringOrUndefined,
|
||||||
objectOrUndefined,
|
objectOrUndefined,
|
||||||
|
JsonValue,
|
||||||
} from "@openfeature/server-sdk";
|
} from "@openfeature/server-sdk";
|
||||||
import type {
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
|
@ -101,6 +102,36 @@ export interface GeneratedClient {
|
||||||
*/
|
*/
|
||||||
greetingMessageDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<string>>;
|
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.
|
* Maximum allowed length for usernames.
|
||||||
*
|
*
|
||||||
|
@ -185,6 +216,14 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
|
||||||
return client.getStringDetails("greetingMessage", "Hello there!", context, options);
|
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> => {
|
usernameMaxLength: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number> => {
|
||||||
return client.getNumberValue("usernameMaxLength", 50, context, options);
|
return client.getNumberValue("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,
|
type ReactFlagEvaluationNoSuspenseOptions,
|
||||||
useFlag,
|
useFlag,
|
||||||
useSuspenseFlag,
|
useSuspenseFlag,
|
||||||
|
JsonValue
|
||||||
} from "@openfeature/react-sdk";
|
} from "@openfeature/react-sdk";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +89,33 @@ export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspen
|
||||||
return useSuspenseFlag("greetingMessage", "Hello there!", options);
|
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.
|
* Maximum allowed length for usernames.
|
||||||
*
|
*
|
|
@ -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!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,19 +6,23 @@ import (
|
||||||
|
|
||||||
// Flag name constants to avoid duplication
|
// Flag name constants to avoid duplication
|
||||||
const (
|
const (
|
||||||
DebugFlagName = "debug"
|
DebugFlagName = "debug"
|
||||||
ManifestFlagName = "manifest"
|
ManifestFlagName = "manifest"
|
||||||
OutputFlagName = "output"
|
OutputFlagName = "output"
|
||||||
NoInputFlagName = "no-input"
|
NoInputFlagName = "no-input"
|
||||||
GoPackageFlagName = "package-name"
|
GoPackageFlagName = "package-name"
|
||||||
OverrideFlagName = "override"
|
CSharpNamespaceName = "namespace"
|
||||||
|
OverrideFlagName = "override"
|
||||||
|
JavaPackageFlagName = "package-name"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default values for flags
|
// Default values for flags
|
||||||
const (
|
const (
|
||||||
DefaultManifestPath = "flags.json"
|
DefaultManifestPath = "flags.json"
|
||||||
DefaultOutputPath = ""
|
DefaultOutputPath = ""
|
||||||
DefaultGoPackageName = "openfeature"
|
DefaultGoPackageName = "openfeature"
|
||||||
|
DefaultCSharpNamespace = "OpenFeature"
|
||||||
|
DefaultJavaPackageName = "com.example.openfeature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddRootFlags adds the common flags to the given command
|
// AddRootFlags adds the common flags to the given command
|
||||||
|
@ -38,6 +42,16 @@ func AddGoGenerateFlags(cmd *cobra.Command) {
|
||||||
cmd.Flags().String(GoPackageFlagName, DefaultGoPackageName, "Name of the generated Go package")
|
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
|
// AddInitFlags adds the init command specific flags
|
||||||
func AddInitFlags(cmd *cobra.Command) {
|
func AddInitFlags(cmd *cobra.Command) {
|
||||||
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
|
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
|
||||||
|
@ -61,6 +75,18 @@ func GetGoPackageName(cmd *cobra.Command) string {
|
||||||
return goPackageName
|
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
|
// GetNoInput gets the no-input flag from the given command
|
||||||
func GetNoInput(cmd *cobra.Command) bool {
|
func GetNoInput(cmd *cobra.Command) bool {
|
||||||
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)
|
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/filesystem"
|
"github.com/open-feature/cli/internal/filesystem"
|
||||||
"github.com/open-feature/cli/internal/manifest"
|
"github.com/open-feature/cli/internal/manifest"
|
||||||
|
@ -27,24 +28,24 @@ const (
|
||||||
func (f FlagType) String() string {
|
func (f FlagType) String() string {
|
||||||
switch f {
|
switch f {
|
||||||
case IntType:
|
case IntType:
|
||||||
return "int"
|
return "int"
|
||||||
case FloatType:
|
case FloatType:
|
||||||
return "float"
|
return "float"
|
||||||
case BoolType:
|
case BoolType:
|
||||||
return "bool"
|
return "bool"
|
||||||
case StringType:
|
case StringType:
|
||||||
return "string"
|
return "string"
|
||||||
case ObjectType:
|
case ObjectType:
|
||||||
return "object"
|
return "object"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Flag struct {
|
type Flag struct {
|
||||||
Key string
|
Key string
|
||||||
Type FlagType
|
Type FlagType
|
||||||
Description string
|
Description string
|
||||||
DefaultValue any
|
DefaultValue any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ func Load(manifestPath string) (*Flagset, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if len(validationErrors) > 0 {
|
} else if len(validationErrors) > 0 {
|
||||||
return nil, fmt.Errorf("validation failed: %v", validationErrors)
|
return nil, errors.New(FormatValidationError(validationErrors))
|
||||||
}
|
}
|
||||||
|
|
||||||
var flagset Flagset
|
var flagset Flagset
|
||||||
|
@ -90,9 +91,9 @@ func (fs *Flagset) Filter(unsupportedFlagTypes map[FlagType]bool) *Flagset {
|
||||||
func (fs *Flagset) UnmarshalJSON(data []byte) error {
|
func (fs *Flagset) UnmarshalJSON(data []byte) error {
|
||||||
var manifest struct {
|
var manifest struct {
|
||||||
Flags map[string]struct {
|
Flags map[string]struct {
|
||||||
FlagType string `json:"flagType"`
|
FlagType string `json:"flagType"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
DefaultValue any `json:"defaultValue"`
|
DefaultValue any `json:"defaultValue"`
|
||||||
} `json:"flags"`
|
} `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,3 +133,43 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,16 +17,16 @@ func defaultFuncs() template.FuncMap {
|
||||||
"ToPascal": strcase.ToCamel,
|
"ToPascal": strcase.ToCamel,
|
||||||
// Remapping ToLowerCamel to ToCamel to match the expected behavior
|
// Remapping ToLowerCamel to ToCamel to match the expected behavior
|
||||||
// Ref: See above
|
// Ref: See above
|
||||||
"ToCamel": strcase.ToLowerCamel,
|
"ToCamel": strcase.ToLowerCamel,
|
||||||
"ToKebab": strcase.ToKebab,
|
"ToKebab": strcase.ToKebab,
|
||||||
"ToScreamingKebab": strcase.ToScreamingKebab,
|
"ToScreamingKebab": strcase.ToScreamingKebab,
|
||||||
"ToSnake": strcase.ToSnake,
|
"ToSnake": strcase.ToSnake,
|
||||||
"ToScreamingSnake": strcase.ToScreamingSnake,
|
"ToScreamingSnake": strcase.ToScreamingSnake,
|
||||||
"ToUpper": strings.ToUpper,
|
"ToUpper": strings.ToUpper,
|
||||||
"ToLower": strings.ToLower,
|
"ToLower": strings.ToLower,
|
||||||
"Title": cases.Title,
|
"Title": cases.Title,
|
||||||
"Quote": strconv.Quote,
|
"Quote": strconv.Quote,
|
||||||
"QuoteString": func (input any) any {
|
"QuoteString": func(input any) any {
|
||||||
if str, ok := input.(string); ok {
|
if str, ok := input.(string); ok {
|
||||||
return strconv.Quote(str)
|
return strconv.Quote(str)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,12 @@ package golang
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/flagset"
|
"github.com/open-feature/cli/internal/flagset"
|
||||||
|
@ -30,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string {
|
||||||
return "Boolean"
|
return "Boolean"
|
||||||
case flagset.StringType:
|
case flagset.StringType:
|
||||||
return "String"
|
return "String"
|
||||||
|
case flagset.ObjectType:
|
||||||
|
return "Object"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -45,6 +52,8 @@ func typeString(flagType flagset.FlagType) string {
|
||||||
return "bool"
|
return "bool"
|
||||||
case flagset.FloatType:
|
case flagset.FloatType:
|
||||||
return "float64"
|
return "float64"
|
||||||
|
case flagset.ObjectType:
|
||||||
|
return "map[string]any"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -60,11 +69,68 @@ func supportImports(flags []flagset.Flag) []string {
|
||||||
return 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 {
|
func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"SupportImports": supportImports,
|
"SupportImports": supportImports,
|
||||||
"OpenFeatureType": openFeatureType,
|
"OpenFeatureType": openFeatureType,
|
||||||
"TypeString": typeString,
|
"TypeString": typeString,
|
||||||
|
"ToMapLiteral": toMapLiteral,
|
||||||
}
|
}
|
||||||
|
|
||||||
newParams := &generators.Params[any]{
|
newParams := &generators.Params[any]{
|
||||||
|
@ -80,8 +146,6 @@ func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
// NewGenerator creates a generator for Go.
|
// NewGenerator creates a generator for Go.
|
||||||
func NewGenerator(fs *flagset.Flagset) *GolangGenerator {
|
func NewGenerator(fs *flagset.Flagset) *GolangGenerator {
|
||||||
return &GolangGenerator{
|
return &GolangGenerator{
|
||||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||||
flagset.ObjectType: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext
|
||||||
type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
|
type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
|
||||||
type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, 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 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
|
var client openfeature.IClient = nil
|
||||||
|
|
||||||
|
@ -29,11 +31,11 @@ var {{ .Key | ToPascal }} = struct {
|
||||||
// the evaluation error, if any, and the evaluation details.
|
// the evaluation error, if any, and the evaluation details.
|
||||||
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
|
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
|
||||||
}{
|
}{
|
||||||
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{ .Type | TypeString }}, error) {
|
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 }}, {{ .DefaultValue | QuoteString }}, evalCtx)
|
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
|
||||||
},
|
},
|
||||||
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{ .Type | OpenFeatureType }}EvaluationDetails, error){
|
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 }}, {{ .DefaultValue | QuoteString }}, evalCtx)
|
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,10 +12,10 @@ type GeneratorCreator func() *cobra.Command
|
||||||
|
|
||||||
// GeneratorInfo contains metadata about a generator
|
// GeneratorInfo contains metadata about a generator
|
||||||
type GeneratorInfo struct {
|
type GeneratorInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Stability Stability
|
Stability Stability
|
||||||
Creator GeneratorCreator
|
Creator GeneratorCreator
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratorManager maintains a registry of available generators
|
// GeneratorManager maintains a registry of available generators
|
||||||
|
@ -34,10 +34,10 @@ func NewGeneratorManager() *GeneratorManager {
|
||||||
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
|
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
|
||||||
cmd := cmdCreator()
|
cmd := cmdCreator()
|
||||||
m.generators[cmd.Use] = GeneratorInfo{
|
m.generators[cmd.Use] = GeneratorInfo{
|
||||||
Name: cmd.Use,
|
Name: cmd.Use,
|
||||||
Description: cmd.Short,
|
Description: cmd.Short,
|
||||||
Stability: Stability(cmd.Annotations["stability"]),
|
Stability: Stability(cmd.Annotations["stability"]),
|
||||||
Creator: cmdCreator,
|
Creator: cmdCreator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 -}}
|
|
@ -2,6 +2,7 @@ package nodejs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/flagset"
|
"github.com/open-feature/cli/internal/flagset"
|
||||||
|
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
|
||||||
return "boolean"
|
return "boolean"
|
||||||
case flagset.StringType:
|
case flagset.StringType:
|
||||||
return "string"
|
return "string"
|
||||||
|
case flagset.ObjectType:
|
||||||
|
return "object"
|
||||||
default:
|
default:
|
||||||
return ""
|
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 {
|
func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"OpenFeatureType": openFeatureType,
|
"OpenFeatureType": openFeatureType,
|
||||||
|
"ToJSONString": toJSONString,
|
||||||
}
|
}
|
||||||
|
|
||||||
newParams := &generators.Params[any]{
|
newParams := &generators.Params[any]{
|
||||||
|
@ -49,8 +61,6 @@ func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
// NewGenerator creates a generator for NodeJS.
|
// NewGenerator creates a generator for NodeJS.
|
||||||
func NewGenerator(fs *flagset.Flagset) *NodejsGenerator {
|
func NewGenerator(fs *flagset.Flagset) *NodejsGenerator {
|
||||||
return &NodejsGenerator{
|
return &NodejsGenerator{
|
||||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||||
flagset.ObjectType: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
OpenFeature,
|
OpenFeature,
|
||||||
stringOrUndefined,
|
stringOrUndefined,
|
||||||
objectOrUndefined,
|
objectOrUndefined,
|
||||||
|
JsonValue,
|
||||||
} from "@openfeature/server-sdk";
|
} from "@openfeature/server-sdk";
|
||||||
import type {
|
import type {
|
||||||
EvaluationContext,
|
EvaluationContext,
|
||||||
|
@ -17,30 +18,30 @@ export interface GeneratedClient {
|
||||||
*
|
*
|
||||||
* **Details:**
|
* **Details:**
|
||||||
* - flag key: `{{ .Key }}`
|
* - flag key: `{{ .Key }}`
|
||||||
* - default value: `{{ .DefaultValue }}`
|
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||||
* - type: `{{ .Type | OpenFeatureType }}`
|
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||||
*
|
*
|
||||||
* Performs a flag evaluation that returns a {{ .Type | OpenFeatureType }}.
|
* Performs a flag evaluation that returns a {{ .Type | OpenFeatureType }}.
|
||||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||||
* @returns {Promise<{{ .Type | OpenFeatureType }}>} Flag evaluation response
|
* @returns {Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>} Flag evaluation response
|
||||||
*/
|
*/
|
||||||
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}>;
|
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{ .Description }}
|
* {{ .Description }}
|
||||||
*
|
*
|
||||||
* **Details:**
|
* **Details:**
|
||||||
* - flag key: `{{ .Key }}`
|
* - flag key: `{{ .Key }}`
|
||||||
* - default value: `{{ .DefaultValue }}`
|
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||||
* - type: `{{ .Type | OpenFeatureType }}`
|
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||||
*
|
*
|
||||||
* Performs a flag evaluation that a returns an evaluation details object.
|
* Performs a flag evaluation that a returns an evaluation details object.
|
||||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||||
* @returns {Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>} Flag evaluation details response
|
* @returns {Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>} Flag evaluation details response
|
||||||
*/
|
*/
|
||||||
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>;
|
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>;
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,12 +75,12 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{{- range .Flagset.Flags }}
|
{{- range .Flagset.Flags }}
|
||||||
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}> => {
|
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> => {
|
||||||
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options);
|
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
{{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>> => {
|
{{ .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 }}, {{ .DefaultValue | QuoteString }}, context, options);
|
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
|
||||||
},
|
},
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
{{ printf " " }}}
|
{{ 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)
|
|
@ -2,6 +2,7 @@ package react
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/flagset"
|
"github.com/open-feature/cli/internal/flagset"
|
||||||
|
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
|
||||||
return "boolean"
|
return "boolean"
|
||||||
case flagset.StringType:
|
case flagset.StringType:
|
||||||
return "string"
|
return "string"
|
||||||
|
case flagset.ObjectType:
|
||||||
|
return "object"
|
||||||
default:
|
default:
|
||||||
return ""
|
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 {
|
func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
funcs := template.FuncMap{
|
funcs := template.FuncMap{
|
||||||
"OpenFeatureType": openFeatureType,
|
"OpenFeatureType": openFeatureType,
|
||||||
|
"ToJSONString": toJSONString,
|
||||||
}
|
}
|
||||||
|
|
||||||
newParams := &generators.Params[any]{
|
newParams := &generators.Params[any]{
|
||||||
|
@ -49,8 +61,6 @@ func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
|
||||||
// NewGenerator creates a generator for React.
|
// NewGenerator creates a generator for React.
|
||||||
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
|
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
|
||||||
return &ReactGenerator{
|
return &ReactGenerator{
|
||||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||||
flagset.ObjectType: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
type ReactFlagEvaluationNoSuspenseOptions,
|
type ReactFlagEvaluationNoSuspenseOptions,
|
||||||
useFlag,
|
useFlag,
|
||||||
useSuspenseFlag,
|
useSuspenseFlag,
|
||||||
|
JsonValue
|
||||||
} from "@openfeature/react-sdk";
|
} from "@openfeature/react-sdk";
|
||||||
{{ range .Flagset.Flags }}
|
{{ range .Flagset.Flags }}
|
||||||
/**
|
/**
|
||||||
|
@ -12,11 +13,11 @@ import {
|
||||||
*
|
*
|
||||||
* **Details:**
|
* **Details:**
|
||||||
* - flag key: `{{ .Key }}`
|
* - flag key: `{{ .Key }}`
|
||||||
* - default value: `{{ .DefaultValue }}`
|
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||||
* - type: `{{ .Type | OpenFeatureType }}`
|
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||||
*/
|
*/
|
||||||
export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => {
|
export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => {
|
||||||
return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
|
return useFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,13 +25,13 @@ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) =
|
||||||
*
|
*
|
||||||
* **Details:**
|
* **Details:**
|
||||||
* - flag key: `{{ .Key }}`
|
* - flag key: `{{ .Key }}`
|
||||||
* - default value: `{{ .DefaultValue }}`
|
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||||
* - type: `{{ .Type | OpenFeatureType }}`
|
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||||
*
|
*
|
||||||
* Equivalent to useFlag with options: `{ suspend: true }`
|
* Equivalent to useFlag with options: `{ suspend: true }`
|
||||||
* @experimental — Suspense is an experimental feature subject to change in future versions.
|
* @experimental — Suspense is an experimental feature subject to change in future versions.
|
||||||
*/
|
*/
|
||||||
export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
|
export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
|
||||||
return useSuspenseFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
|
return useSuspenseFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
|
||||||
};
|
};
|
||||||
{{ end}}
|
{{ end}}
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -63,9 +63,9 @@ type Manifest struct {
|
||||||
// Converts the Manifest struct to a JSON schema.
|
// Converts the Manifest struct to a JSON schema.
|
||||||
func ToJSONSchema() *jsonschema.Schema {
|
func ToJSONSchema() *jsonschema.Schema {
|
||||||
reflector := &jsonschema.Reflector{
|
reflector := &jsonschema.Reflector{
|
||||||
ExpandedStruct: true,
|
ExpandedStruct: true,
|
||||||
AllowAdditionalProperties: true,
|
AllowAdditionalProperties: true,
|
||||||
BaseSchemaID: "openfeature-cli",
|
BaseSchemaID: "openfeature-cli",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
|
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/open-feature/cli/internal/filesystem"
|
"github.com/open-feature/cli/internal/filesystem"
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
type initManifest struct {
|
type initManifest struct {
|
||||||
|
@ -14,7 +15,7 @@ type initManifest struct {
|
||||||
// Create creates a new manifest file at the given path.
|
// Create creates a new manifest file at the given path.
|
||||||
func Create(path string) error {
|
func Create(path string) error {
|
||||||
m := &initManifest{
|
m := &initManifest{
|
||||||
Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
|
Schema: "https://raw.githubusercontent.com/open-feature/cli/main/schema/v0/flag-manifest.json",
|
||||||
Manifest: Manifest{
|
Manifest: Manifest{
|
||||||
Flags: map[string]any{},
|
Flags: map[string]any{},
|
||||||
},
|
},
|
||||||
|
@ -25,3 +26,19 @@ func Create(path string) error {
|
||||||
}
|
}
|
||||||
return filesystem.WriteFile(path, formattedInitManifest)
|
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,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"]
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ func main() {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if _, err := file.Write(data); err != nil {
|
if _, err := file.Write(data); err != nil {
|
||||||
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err));
|
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("JSON schema generated successfully at " + schemaPath)
|
fmt.Println("JSON schema generated successfully at " + schemaPath)
|
||||||
|
|
|
@ -49,11 +49,11 @@ func walkPath(shouldPass bool, root string) error {
|
||||||
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
|
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
|
||||||
manifestLoader := gojsonschema.NewGoLoader(v)
|
manifestLoader := gojsonschema.NewGoLoader(v)
|
||||||
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
|
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
|
||||||
if (err != nil) {
|
if err != nil {
|
||||||
return fmt.Errorf("Error validating json schema: %v", err)
|
return fmt.Errorf("Error validating json schema: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len(result.Errors()) >= 1 && shouldPass == true) {
|
if len(result.Errors()) >= 1 && shouldPass == true {
|
||||||
var errorMessage strings.Builder
|
var errorMessage strings.Builder
|
||||||
|
|
||||||
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
|
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
|
||||||
|
@ -63,7 +63,7 @@ func walkPath(shouldPass bool, root string) error {
|
||||||
return fmt.Errorf("%s", errorMessage.String())
|
return fmt.Errorf("%s", errorMessage.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len(result.Errors()) == 0 && shouldPass == false) {
|
if len(result.Errors()) == 0 && shouldPass == false {
|
||||||
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
|
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# OpenFeature CLI Integration Testing
|
||||||
|
|
||||||
|
This directory contains integration tests for validating the OpenFeature CLI generators.
|
||||||
|
|
||||||
|
## Integration Test Structure
|
||||||
|
|
||||||
|
The integration tests use [Dagger](https://dagger.io/) to create reproducible test environments without needing to install dependencies locally.
|
||||||
|
|
||||||
|
Each integration test:
|
||||||
|
|
||||||
|
1. Builds the CLI from source
|
||||||
|
2. Generates code using a sample manifest file
|
||||||
|
3. Compiles and tests the generated code in a language-specific container
|
||||||
|
4. Reports success or failure
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Run all integration tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run a specific integration test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For C# tests
|
||||||
|
make test-csharp-dagger
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a New Integration Test
|
||||||
|
|
||||||
|
To add an integration test for a new generator:
|
||||||
|
|
||||||
|
1. Create a combined implementation and runner file in `test/integration/cmd/<language>/run.go`
|
||||||
|
2. Update the main runner in `test/integration/cmd/run.go` to execute your new test
|
||||||
|
3. Add a Makefile target for running your test individually
|
||||||
|
|
||||||
|
See the step-by-step guide in [new-language.md](new-language.md) for detailed instructions.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The testing framework uses the following components:
|
||||||
|
|
||||||
|
- `test/integration/integration.go`: Defines the `Test` interface and common utilities
|
||||||
|
- `test/integration/cmd/run.go`: Runner for all integration tests that executes each language-specific test
|
||||||
|
- `test/integration/cmd/<language>/run.go`: Combined implementation and runner for each language
|
||||||
|
- `test/<language>-integration/`: Contains language-specific test files (code samples, project files)
|
||||||
|
|
||||||
|
Each integration test uses Dagger to:
|
||||||
|
|
||||||
|
1. Build the CLI in a clean environment
|
||||||
|
2. Generate code using a sample manifest
|
||||||
|
3. Compile and test the generated code in a language-specific container
|
||||||
|
4. Report success or failure
|
||||||
|
|
||||||
|
## Benefits Over Shell Scripts
|
||||||
|
|
||||||
|
Using Dagger for integration tests provides several advantages:
|
||||||
|
|
||||||
|
1. **Reproducibility**: Tests run in containerized environments that are identical locally and in CI
|
||||||
|
2. **Language Support**: Easy to add new language tests with the same pattern
|
||||||
|
3. **Improved Debugging**: Clear separation of build, generate, and test steps
|
||||||
|
4. **Parallelization**: Tests can run in parallel when executed in different containers
|
||||||
|
5. **No Dependencies**: No need to install language-specific tooling locally
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||||
|
<PackageReference Include="OpenFeature" Version="2.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="generated\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,17 @@
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY expected/OpenFeature.cs /app/
|
||||||
|
COPY CompileTest.csproj /app/
|
||||||
|
COPY Program.cs /app/
|
||||||
|
|
||||||
|
# Restore dependencies
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
RUN dotnet build
|
||||||
|
|
||||||
|
# The image will be used to validate C# compilation only
|
||||||
|
ENTRYPOINT ["dotnet", "run"]
|
|
@ -0,0 +1,208 @@
|
||||||
|
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenFeature;
|
||||||
|
using OpenFeature.Model;
|
||||||
|
|
||||||
|
namespace OpenFeature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generated OpenFeature client for typesafe flag access
|
||||||
|
/// </summary>
|
||||||
|
public class GeneratedClient
|
||||||
|
{
|
||||||
|
private readonly IFeatureClient _client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
|
||||||
|
public GeneratedClient(IFeatureClient client)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Discount percentage applied to purchases.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: discountPercentage</para>
|
||||||
|
/// <para>Default value: 0.15</para>
|
||||||
|
/// <para>Type: float</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<float> DiscountPercentageAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetFloatValueAsync("discountPercentage", 0.15, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discount percentage applied to purchases.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: discountPercentage</para>
|
||||||
|
/// <para>Default value: 0.15</para>
|
||||||
|
/// <para>Type: float</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<EvaluationDetails<float>> DiscountPercentageDetailsAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetFloatDetailsAsync("discountPercentage", 0.15, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether Feature A is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: enableFeatureA</para>
|
||||||
|
/// <para>Default value: false</para>
|
||||||
|
/// <para>Type: bool</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<bool> EnableFeatureAAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetBoolValueAsync("enableFeatureA", false, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether Feature A is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: enableFeatureA</para>
|
||||||
|
/// <para>Default value: false</para>
|
||||||
|
/// <para>Type: bool</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<EvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetBoolDetailsAsync("enableFeatureA", false, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message to use for greeting users.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: greetingMessage</para>
|
||||||
|
/// <para>Default value: Hello there!</para>
|
||||||
|
/// <para>Type: string</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<string> GreetingMessageAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message to use for greeting users.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: greetingMessage</para>
|
||||||
|
/// <para>Default value: Hello there!</para>
|
||||||
|
/// <para>Type: string</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<EvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows customization of theme colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: themeCustomization</para>
|
||||||
|
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
|
||||||
|
/// <para>Type: object</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows customization of theme colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: themeCustomization</para>
|
||||||
|
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
|
||||||
|
/// <para>Type: object</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum allowed length for usernames.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: usernameMaxLength</para>
|
||||||
|
/// <para>Default value: 50</para>
|
||||||
|
/// <para>Type: int</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<int> UsernameMaxLengthAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetIntValueAsync("usernameMaxLength", 50, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum allowed length for usernames.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: usernameMaxLength</para>
|
||||||
|
/// <para>Default value: 50</para>
|
||||||
|
/// <para>Type: int</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<EvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext evaluationContext = null)
|
||||||
|
{
|
||||||
|
return await _client.GetIntDetailsAsync("usernameMaxLength", 50, evaluationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using the default OpenFeature client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient()
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.GetClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="domain">The domain to get the client for</param>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient(string domain)
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.GetClient(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="domain">The domain to get the client for</param>
|
||||||
|
/// <param name="evaluationContext">Default context to use for evaluations</param>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient(string domain, EvaluationContext evaluationContext)
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.GetClient(domain, evaluationContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OpenFeature;
|
||||||
|
using OpenFeature.Model;
|
||||||
|
using TestNamespace;
|
||||||
|
|
||||||
|
// This program just validates that the generated OpenFeature C# client code compiles
|
||||||
|
// We don't need to run the code since the goal is to test compilation only
|
||||||
|
namespace CompileTest
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Testing compilation of generated OpenFeature client...");
|
||||||
|
|
||||||
|
// Test DI initialization
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
// Register OpenFeature services manually for the test
|
||||||
|
services.AddSingleton(_ => Api.Instance);
|
||||||
|
services.AddSingleton<IFeatureClient>(_ => Api.Instance.GetClient());
|
||||||
|
services.AddSingleton<GeneratedClient>();
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Test client retrieval from DI
|
||||||
|
var client = serviceProvider.GetRequiredService<GeneratedClient>();
|
||||||
|
|
||||||
|
// Also test the traditional factory method
|
||||||
|
var clientFromFactory = GeneratedClient.CreateClient();
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
Console.WriteLine("Generated C# code compiles successfully!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
# C# Integration Testing
|
||||||
|
|
||||||
|
This directory contains integration tests for the C# code generator.
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
Run the C# integration tests with Dagger:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-csharp-dagger
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Build the OpenFeature CLI
|
||||||
|
2. Generate C# client code using the sample manifest
|
||||||
|
3. Run the C# compilation test in an isolated environment
|
||||||
|
4. Report success or failure
|
||||||
|
|
||||||
|
## What the test does
|
||||||
|
|
||||||
|
The integration test:
|
||||||
|
1. Builds the OpenFeature CLI inside a container
|
||||||
|
2. Generates C# client code using a sample manifest
|
||||||
|
3. Compiles the generated code with a sample program
|
||||||
|
4. Runs the compiled program to verify it works correctly
|
||||||
|
|
||||||
|
## Test Files
|
||||||
|
|
||||||
|
- `CompileTest.csproj`: .NET project file for compilation testing
|
||||||
|
- `Program.cs`: Test program that uses the generated code
|
||||||
|
- `expected/`: Directory containing expected output files (used for verification)
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The C# integration test uses Dagger to create a reproducible test environment:
|
||||||
|
|
||||||
|
1. It builds the CLI in a Go container
|
||||||
|
2. Generates C# code using the CLI
|
||||||
|
3. Tests the generated code in a .NET container
|
||||||
|
|
||||||
|
The implementation is located in `test/integration/cmd/csharp/run.go`.
|
||||||
|
|
||||||
|
For more implementation details, see the main [test/README.md](../README.md) file.
|
|
@ -0,0 +1,252 @@
|
||||||
|
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OpenFeature;
|
||||||
|
using OpenFeature.Model;
|
||||||
|
|
||||||
|
|
||||||
|
namespace TestNamespace
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service collection extensions for OpenFeature
|
||||||
|
/// </summary>
|
||||||
|
public static class OpenFeatureServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds OpenFeature services to the service collection with the generated client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to add services to</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddOpenFeature(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddSingleton(_ => Api.Instance)
|
||||||
|
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient())
|
||||||
|
.AddSingleton<GeneratedClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds OpenFeature services to the service collection with the generated client for a specific domain
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection to add services to</param>
|
||||||
|
/// <param name="domain">The domain to get the client for</param>
|
||||||
|
/// <returns>The service collection for chaining</returns>
|
||||||
|
public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddSingleton(_ => Api.Instance)
|
||||||
|
.AddSingleton(provider => provider.GetRequiredService<Api>().GetClient(domain))
|
||||||
|
.AddSingleton<GeneratedClient>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generated OpenFeature client for typesafe flag access
|
||||||
|
/// </summary>
|
||||||
|
public class GeneratedClient
|
||||||
|
{
|
||||||
|
private readonly IFeatureClient _client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GeneratedClient"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The OpenFeature client to use for flag evaluations.</param>
|
||||||
|
public GeneratedClient(IFeatureClient client)
|
||||||
|
{
|
||||||
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Discount percentage applied to purchases.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: discountPercentage</para>
|
||||||
|
/// <para>Default value: 0.15</para>
|
||||||
|
/// <para>Type: double</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<double> DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discount percentage applied to purchases.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: discountPercentage</para>
|
||||||
|
/// <para>Default value: 0.15</para>
|
||||||
|
/// <para>Type: double</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<double>> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether Feature A is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: enableFeatureA</para>
|
||||||
|
/// <para>Default value: false</para>
|
||||||
|
/// <para>Type: bool</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<bool> EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether Feature A is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: enableFeatureA</para>
|
||||||
|
/// <para>Default value: false</para>
|
||||||
|
/// <para>Type: bool</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<bool>> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message to use for greeting users.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: greetingMessage</para>
|
||||||
|
/// <para>Default value: Hello there!</para>
|
||||||
|
/// <para>Type: string</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<string> GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message to use for greeting users.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: greetingMessage</para>
|
||||||
|
/// <para>Default value: Hello there!</para>
|
||||||
|
/// <para>Type: string</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<string>> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows customization of theme colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: themeCustomization</para>
|
||||||
|
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
|
||||||
|
/// <para>Type: object</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<Value> ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows customization of theme colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: themeCustomization</para>
|
||||||
|
/// <para>Default value: new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build())</para>
|
||||||
|
/// <para>Type: object</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<Value>> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum allowed length for usernames.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: usernameMaxLength</para>
|
||||||
|
/// <para>Default value: 50</para>
|
||||||
|
/// <para>Type: int</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The flag value</returns>
|
||||||
|
public async Task<int> UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum allowed length for usernames.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>Flag key: usernameMaxLength</para>
|
||||||
|
/// <para>Default value: 50</para>
|
||||||
|
/// <para>Type: int</para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="evaluationContext">Optional context for the flag evaluation</param>
|
||||||
|
/// <param name="options">Options for flag evaluation</param>
|
||||||
|
/// <returns>The evaluation details containing the flag value and metadata</returns>
|
||||||
|
public async Task<FlagEvaluationDetails<int>> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
|
||||||
|
{
|
||||||
|
return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using the default OpenFeature client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient()
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.Instance.GetClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using a domain-specific OpenFeature client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="domain">The domain to get the client for</param>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient(string domain)
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.Instance.GetClient(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="domain">The domain to get the client for</param>
|
||||||
|
/// <param name="evaluationContext">Default context to use for evaluations</param>
|
||||||
|
/// <returns>A new GeneratedClient instance</returns>
|
||||||
|
public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
|
||||||
|
{
|
||||||
|
return new GeneratedClient(Api.Instance.GetClient(domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
module github.com/open-feature/cli/test/go-integration
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/open-feature/go-sdk v1.15.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,106 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
generated "github.com/open-feature/cli/test/go-integration/openfeature"
|
||||||
|
"github.com/open-feature/go-sdk/openfeature"
|
||||||
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
// Set up the in-memory provider with test flags
|
||||||
|
provider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
|
||||||
|
"discountPercentage": {
|
||||||
|
State: memprovider.Enabled,
|
||||||
|
DefaultVariant: "default",
|
||||||
|
Variants: map[string]any{
|
||||||
|
"default": 0.15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"enableFeatureA": {
|
||||||
|
State: memprovider.Enabled,
|
||||||
|
DefaultVariant: "default",
|
||||||
|
Variants: map[string]any{
|
||||||
|
"default": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"greetingMessage": {
|
||||||
|
State: memprovider.Enabled,
|
||||||
|
DefaultVariant: "default",
|
||||||
|
Variants: map[string]any{
|
||||||
|
"default": "Hello there!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"usernameMaxLength": {
|
||||||
|
State: memprovider.Enabled,
|
||||||
|
DefaultVariant: "default",
|
||||||
|
Variants: map[string]any{
|
||||||
|
"default": 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"themeCustomization": {
|
||||||
|
State: memprovider.Enabled,
|
||||||
|
DefaultVariant: "default",
|
||||||
|
Variants: map[string]any{
|
||||||
|
"default": map[string]any{
|
||||||
|
"primaryColor": "#007bff",
|
||||||
|
"secondaryColor": "#6c757d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set the provider and wait for it to be ready
|
||||||
|
err := openfeature.SetProviderAndWait(provider)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to set provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
evalCtx := openfeature.NewEvaluationContext("someid", map[string]any{})
|
||||||
|
|
||||||
|
// Use the generated code for all flag evaluations
|
||||||
|
enableFeatureA, err := generated.EnableFeatureA.Value(ctx, evalCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error evaluating boolean flag: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("enableFeatureA: %v\n", enableFeatureA)
|
||||||
|
|
||||||
|
discount, err := generated.DiscountPercentage.Value(ctx, evalCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get discount: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Discount Percentage: %.2f\n", discount)
|
||||||
|
|
||||||
|
greetingMessage, err := generated.GreetingMessage.Value(ctx, evalCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error evaluating string flag: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("greetingMessage: %v\n", greetingMessage)
|
||||||
|
|
||||||
|
usernameMaxLength, err := generated.UsernameMaxLength.Value(ctx, evalCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error evaluating int flag: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("usernameMaxLength: %v\n", usernameMaxLength)
|
||||||
|
|
||||||
|
themeCustomization, err := generated.ThemeCustomization.Value(ctx, evalCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error evaluating int flag: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("themeCustomization: %v\n", themeCustomization)
|
||||||
|
|
||||||
|
fmt.Println("Generated Go code compiles successfully!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"dagger.io/dagger"
|
||||||
|
"github.com/open-feature/cli/test/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test implements the integration test for the C# generator
|
||||||
|
type Test struct {
|
||||||
|
// ProjectDir is the absolute path to the root of the project
|
||||||
|
ProjectDir string
|
||||||
|
// TestDir is the absolute path to the test directory
|
||||||
|
TestDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Test
|
||||||
|
func New(projectDir, testDir string) *Test {
|
||||||
|
return &Test{
|
||||||
|
ProjectDir: projectDir,
|
||||||
|
TestDir: testDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the C# integration test using Dagger
|
||||||
|
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
|
||||||
|
// Source code container
|
||||||
|
source := client.Host().Directory(t.ProjectDir)
|
||||||
|
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
|
||||||
|
Include: []string{"CompileTest.csproj", "Program.cs"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build the CLI
|
||||||
|
cli := client.Container().
|
||||||
|
From("golang:1.24-alpine").
|
||||||
|
WithDirectory("/src", source).
|
||||||
|
WithWorkdir("/src").
|
||||||
|
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
|
||||||
|
|
||||||
|
// Generate C# client
|
||||||
|
generated := cli.WithExec([]string{
|
||||||
|
"./cli", "generate", "csharp",
|
||||||
|
"--manifest=/src/sample/sample_manifest.json",
|
||||||
|
"--output=/tmp/generated",
|
||||||
|
"--namespace=TestNamespace",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get generated files
|
||||||
|
generatedFiles := generated.Directory("/tmp/generated")
|
||||||
|
|
||||||
|
// Test C# compilation with the generated files
|
||||||
|
dotnetContainer := client.Container().
|
||||||
|
From("mcr.microsoft.com/dotnet/sdk:8.0").
|
||||||
|
WithDirectory("/app/generated", generatedFiles).
|
||||||
|
WithDirectory("/app", testFiles).
|
||||||
|
WithWorkdir("/app").
|
||||||
|
WithExec([]string{"dotnet", "restore"}).
|
||||||
|
WithExec([]string{"dotnet", "build"}).
|
||||||
|
WithExec([]string{"dotnet", "run"})
|
||||||
|
|
||||||
|
return dotnetContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the integration test
|
||||||
|
func (t *Test) Name() string {
|
||||||
|
return "csharp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Get project root
|
||||||
|
projectDir, err := filepath.Abs(os.Getenv("PWD"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get test directory
|
||||||
|
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/csharp-integration"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and run the C# integration test
|
||||||
|
test := New(projectDir, testDir)
|
||||||
|
|
||||||
|
if err := integration.RunTest(ctx, test); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"dagger.io/dagger"
|
||||||
|
"github.com/open-feature/cli/test/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test implements the integration test for the Go generator
|
||||||
|
type Test struct {
|
||||||
|
// ProjectDir is the absolute path to the root of the project
|
||||||
|
ProjectDir string
|
||||||
|
// TestDir is the absolute path to the test directory
|
||||||
|
TestDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Test
|
||||||
|
func New(projectDir, testDir string) *Test {
|
||||||
|
return &Test{
|
||||||
|
ProjectDir: projectDir,
|
||||||
|
TestDir: testDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the Go integration test using Dagger
|
||||||
|
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
|
||||||
|
// Source code container
|
||||||
|
source := client.Host().Directory(t.ProjectDir)
|
||||||
|
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
|
||||||
|
Include: []string{"test.go", "go.mod"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build the CLI
|
||||||
|
cli := client.Container().
|
||||||
|
From("golang:1.23-alpine").
|
||||||
|
WithExec([]string{"apk", "add", "--no-cache", "git"}).
|
||||||
|
WithDirectory("/src", source).
|
||||||
|
WithWorkdir("/src").
|
||||||
|
WithExec([]string{"go", "mod", "tidy"}).
|
||||||
|
WithExec([]string{"go", "mod", "download"}).
|
||||||
|
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
|
||||||
|
|
||||||
|
// Generate Go client
|
||||||
|
generated := cli.WithExec([]string{
|
||||||
|
"./cli", "generate", "go",
|
||||||
|
"--manifest=/src/sample/sample_manifest.json",
|
||||||
|
"--output=/tmp/generated",
|
||||||
|
"--package-name=openfeature",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get generated files
|
||||||
|
generatedFiles := generated.Directory("/tmp/generated")
|
||||||
|
|
||||||
|
// Test Go compilation with the generated files
|
||||||
|
goContainer := client.Container().
|
||||||
|
From("golang:1.23-alpine").
|
||||||
|
WithExec([]string{"apk", "add", "--no-cache", "git"}).
|
||||||
|
WithWorkdir("/app").
|
||||||
|
WithDirectory("/app", testFiles).
|
||||||
|
WithDirectory("/app/openfeature", generatedFiles).
|
||||||
|
WithExec([]string{"go", "mod", "tidy"}).
|
||||||
|
WithExec([]string{"go", "build", "-o", "test", "-v"}).
|
||||||
|
WithExec([]string{"./test"})
|
||||||
|
|
||||||
|
return goContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the integration test
|
||||||
|
func (t *Test) Name() string {
|
||||||
|
return "go"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Get project root
|
||||||
|
projectDir, err := filepath.Abs(os.Getenv("PWD"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get test directory
|
||||||
|
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/go-integration"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and run the Go integration test
|
||||||
|
test := New(projectDir, testDir)
|
||||||
|
|
||||||
|
if err := integration.RunTest(ctx, test); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"dagger.io/dagger"
|
||||||
|
"fmt"
|
||||||
|
"github.com/open-feature/cli/test/integration"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
ProjectDir string
|
||||||
|
TestDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(projectDir, testDir string) *Test {
|
||||||
|
return &Test{
|
||||||
|
ProjectDir: projectDir,
|
||||||
|
TestDir: testDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
|
||||||
|
source := client.Host().Directory(t.ProjectDir)
|
||||||
|
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
|
||||||
|
Include: []string{"package.json", "test.ts"},
|
||||||
|
})
|
||||||
|
|
||||||
|
cli := client.Container().
|
||||||
|
From("golang:1.24-alpine").
|
||||||
|
WithDirectory("/src", source).
|
||||||
|
WithWorkdir("/src").
|
||||||
|
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
|
||||||
|
|
||||||
|
generated := cli.WithExec([]string{
|
||||||
|
"./cli", "generate", "nodejs",
|
||||||
|
"--manifest=/src/sample/sample_manifest.json",
|
||||||
|
"--output=/tmp/generated",
|
||||||
|
})
|
||||||
|
|
||||||
|
generatedFiles := generated.Directory("/tmp/generated")
|
||||||
|
|
||||||
|
nodeContainer := client.Container().
|
||||||
|
From("node:22-alpine").
|
||||||
|
WithExec([]string{"npm", "install", "-g", "typescript"}).
|
||||||
|
WithDirectory("/app/generated", generatedFiles).
|
||||||
|
WithDirectory("/app", testFiles).
|
||||||
|
WithWorkdir("/app").
|
||||||
|
WithExec([]string{"npm", "install"}).
|
||||||
|
WithExec([]string{"npm", "test"})
|
||||||
|
|
||||||
|
return nodeContainer, nil
|
||||||
|
}
|
||||||
|
func (t *Test) Name() string {
|
||||||
|
return "nodejs"
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
projectDir, err := filepath.Abs(os.Getenv("PWD"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/nodejs-integration"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
test := New(projectDir, testDir)
|
||||||
|
|
||||||
|
if err := integration.RunTest(ctx, test); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// Run the language-specific tests
|
||||||
|
fmt.Println("=== Running all integration tests ===")
|
||||||
|
|
||||||
|
// Run the C# integration test
|
||||||
|
csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp")
|
||||||
|
csharpCmd.Stdout = os.Stdout
|
||||||
|
csharpCmd.Stderr = os.Stderr
|
||||||
|
if err := csharpCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the Go integration test
|
||||||
|
goCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/go")
|
||||||
|
goCmd.Stdout = os.Stdout
|
||||||
|
goCmd.Stderr = os.Stderr
|
||||||
|
if err := goCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running Go integration test: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
//Run the nodejs test
|
||||||
|
nodeCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/nodejs")
|
||||||
|
nodeCmd.Stdout = os.Stdout
|
||||||
|
nodeCmd.Stderr = os.Stderr
|
||||||
|
if err := nodeCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running nodejs integration test: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more tests here as they are available
|
||||||
|
|
||||||
|
fmt.Println("=== All integration tests passed successfully ===")
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"dagger.io/dagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test defines the interface for all integration tests
|
||||||
|
type Test interface {
|
||||||
|
// Run executes the integration test with the given Dagger client
|
||||||
|
Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error)
|
||||||
|
// Name returns the name of the integration test
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunTest runs a single integration test
|
||||||
|
func RunTest(ctx context.Context, test Test) error {
|
||||||
|
// Initialize Dagger client
|
||||||
|
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to Dagger engine: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
fmt.Printf("=== Running %s integration test ===\n", test.Name())
|
||||||
|
|
||||||
|
// Run the integration test
|
||||||
|
container, err := test.Run(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run %s integration test: %w", test.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the pipeline and wait for it to complete
|
||||||
|
_, err = container.Stdout(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s integration test failed: %w", test.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("=== Success: %s integration test passed ===\n", test.Name())
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
# Adding a New Generator Integration Test
|
||||||
|
|
||||||
|
This guide explains how to add integration tests for a new generator.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
The integration testing framework has the following directory structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
integration/ # Core integration test framework
|
||||||
|
integration.go # Test interface definition
|
||||||
|
cmd/ # Command-line runners and implementations
|
||||||
|
run.go # Runner for all tests
|
||||||
|
csharp/ # C# specific implementation and runner
|
||||||
|
run.go
|
||||||
|
python/ # Python specific implementation and runner (future)
|
||||||
|
run.go
|
||||||
|
csharp-integration/ # C# test files
|
||||||
|
python-integration/ # Python test files (future)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Create a generator-specific implementation and runner
|
||||||
|
|
||||||
|
Create a file at `test/integration/cmd/python/run.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"dagger.io/dagger"
|
||||||
|
"github.com/open-feature/cli/test/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test implements the integration test for the Python generator
|
||||||
|
type Test struct {
|
||||||
|
ProjectDir string
|
||||||
|
TestDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Test
|
||||||
|
func New(projectDir, testDir string) *Test {
|
||||||
|
return &Test{
|
||||||
|
ProjectDir: projectDir,
|
||||||
|
TestDir: testDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the Python integration test
|
||||||
|
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
|
||||||
|
// Source code container
|
||||||
|
source := client.Host().Directory(t.ProjectDir)
|
||||||
|
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
|
||||||
|
Include: []string{"test_openfeature.py", "requirements.txt"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build the CLI
|
||||||
|
cli := client.Container().
|
||||||
|
From("golang:1.24-alpine").
|
||||||
|
WithDirectory("/src", source).
|
||||||
|
WithWorkdir("/src").
|
||||||
|
WithExec([]string{"go", "build", "-o", "cli"})
|
||||||
|
|
||||||
|
// Generate Python client
|
||||||
|
generated := cli.WithExec([]string{
|
||||||
|
"./cli", "generate", "python",
|
||||||
|
"--manifest=/src/sample/sample_manifest.json",
|
||||||
|
"--output=/tmp/generated",
|
||||||
|
"--package=openfeature_test",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get generated files
|
||||||
|
generatedFiles := generated.Directory("/tmp/generated")
|
||||||
|
|
||||||
|
// Test Python with the generated files
|
||||||
|
pythonContainer := client.Container().
|
||||||
|
From("python:3.11-slim").
|
||||||
|
WithDirectory("/app/openfeature", generatedFiles).
|
||||||
|
WithDirectory("/app/test", testFiles).
|
||||||
|
WithWorkdir("/app").
|
||||||
|
WithExec([]string{"pip", "install", "-r", "test/requirements.txt"}).
|
||||||
|
WithExec([]string{"python", "-m", "pytest", "test/test_openfeature.py", "-v"})
|
||||||
|
|
||||||
|
return pythonContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the integration test
|
||||||
|
func (t *Test) Name() string {
|
||||||
|
return "python"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Get project root
|
||||||
|
projectDir, err := filepath.Abs(os.Getenv("PWD"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get test directory
|
||||||
|
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/python-integration"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and run the Python integration test
|
||||||
|
test := New(projectDir, testDir)
|
||||||
|
|
||||||
|
if err := integration.RunTest(ctx, test); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Add the test to the all-integration runner
|
||||||
|
|
||||||
|
Update `test/integration/cmd/run.go` to include your test:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Run the generator-specific tests
|
||||||
|
fmt.Println("=== Running all integration tests ===")
|
||||||
|
|
||||||
|
// Run the C# integration test
|
||||||
|
csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp")
|
||||||
|
csharpCmd.Stdout = os.Stdout
|
||||||
|
csharpCmd.Stderr = os.Stderr
|
||||||
|
if err := csharpCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the Python integration test
|
||||||
|
pythonCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/python")
|
||||||
|
pythonCmd.Stdout = os.Stdout
|
||||||
|
pythonCmd.Stderr = os.Stderr
|
||||||
|
if err := pythonCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error running Python integration test: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more tests here as they are available
|
||||||
|
|
||||||
|
fmt.Println("=== All integration tests passed successfully ===")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Create test files
|
||||||
|
|
||||||
|
Create the following directory structure with your test files:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
python-integration/
|
||||||
|
requirements.txt
|
||||||
|
test_openfeature.py
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Add a Makefile target
|
||||||
|
|
||||||
|
Update the Makefile with a new target:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
.PHONY: test-python-dagger
|
||||||
|
test-python-dagger:
|
||||||
|
@echo "Running Python integration test with Dagger..."
|
||||||
|
@go run ./test/integration/cmd/python/run.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Update the documentation
|
||||||
|
|
||||||
|
Update `test/README.md` to include your new test.
|
|
@ -0,0 +1,598 @@
|
||||||
|
{
|
||||||
|
"name": "nodejs-integration",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "nodejs-integration",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@openfeature/server-sdk": "^1.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.19.0",
|
||||||
|
"tsx": "^4.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@openfeature/core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-FX/B6yMD2s4BlMKtB0PqSMl94eLaTwh0VK9URcMvjww0hqMOeGZnGv4uv9O5E58krAan7yCOCm4NBCoh2IATqw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@openfeature/server-sdk": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@openfeature/server-sdk/-/server-sdk-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-uP8nqEGBS58s3iWXx6d8vnJ6ZVt3DACJL4PWADOAuwIS4xXpID91783e9f6zQ0i1ijQpj3yx+3ZuCB2LfQMUMA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@openfeature/core": "^1.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
|
||||||
|
"integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.25.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||||
|
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.25.5",
|
||||||
|
"@esbuild/android-arm": "0.25.5",
|
||||||
|
"@esbuild/android-arm64": "0.25.5",
|
||||||
|
"@esbuild/android-x64": "0.25.5",
|
||||||
|
"@esbuild/darwin-arm64": "0.25.5",
|
||||||
|
"@esbuild/darwin-x64": "0.25.5",
|
||||||
|
"@esbuild/freebsd-arm64": "0.25.5",
|
||||||
|
"@esbuild/freebsd-x64": "0.25.5",
|
||||||
|
"@esbuild/linux-arm": "0.25.5",
|
||||||
|
"@esbuild/linux-arm64": "0.25.5",
|
||||||
|
"@esbuild/linux-ia32": "0.25.5",
|
||||||
|
"@esbuild/linux-loong64": "0.25.5",
|
||||||
|
"@esbuild/linux-mips64el": "0.25.5",
|
||||||
|
"@esbuild/linux-ppc64": "0.25.5",
|
||||||
|
"@esbuild/linux-riscv64": "0.25.5",
|
||||||
|
"@esbuild/linux-s390x": "0.25.5",
|
||||||
|
"@esbuild/linux-x64": "0.25.5",
|
||||||
|
"@esbuild/netbsd-arm64": "0.25.5",
|
||||||
|
"@esbuild/netbsd-x64": "0.25.5",
|
||||||
|
"@esbuild/openbsd-arm64": "0.25.5",
|
||||||
|
"@esbuild/openbsd-x64": "0.25.5",
|
||||||
|
"@esbuild/sunos-x64": "0.25.5",
|
||||||
|
"@esbuild/win32-arm64": "0.25.5",
|
||||||
|
"@esbuild/win32-ia32": "0.25.5",
|
||||||
|
"@esbuild/win32-x64": "0.25.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||||
|
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.19.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
|
||||||
|
"integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.25.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "nodejs-integration",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Integration test for OpenFeature CLI Node.js generator",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "tsx test.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@openfeature/server-sdk": "^1.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.19.0",
|
||||||
|
"tsx": "^4.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { OpenFeature, JsonValue } from "@openfeature/server-sdk";
|
||||||
|
import { existsSync, readdirSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Simple test provider
|
||||||
|
class TestProvider {
|
||||||
|
get name() { return 'test-provider'; }
|
||||||
|
get metadata() { return { name: 'test-provider' }; }
|
||||||
|
|
||||||
|
async resolveBooleanEvaluation(flagKey: string, defaultValue: boolean) {
|
||||||
|
return { value: flagKey === 'enableFeatureA' ? true : defaultValue, reason: 'STATIC' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveStringEvaluation(flagKey: string, defaultValue: string) {
|
||||||
|
return { value: flagKey === 'greetingMessage' ? 'Hello from test!' : defaultValue, reason: 'STATIC' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveNumberEvaluation(flagKey: string, defaultValue: number) {
|
||||||
|
const values: Record<string, number> = { usernameMaxLength: 100, discountPercentage: 0.25 };
|
||||||
|
return { value: values[flagKey] ?? defaultValue, reason: 'STATIC' };
|
||||||
|
}
|
||||||
|
async resolveObjectEvaluation<T>(flagKey: string, defaultValue: JsonValue) {
|
||||||
|
const values: Record<string, JsonValue> = {
|
||||||
|
themeCustomization: {
|
||||||
|
primaryColor: '#ff0000',
|
||||||
|
secondaryColor: '#00ff00'
|
||||||
|
} as T
|
||||||
|
};
|
||||||
|
return { value: values[flagKey] ?? defaultValue, reason: 'STATIC' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 Node.js OpenFeature Integration Test');
|
||||||
|
|
||||||
|
// 1. Check generated files
|
||||||
|
const generatedDir = join(__dirname, 'generated');
|
||||||
|
if (!existsSync(generatedDir)) {
|
||||||
|
throw new Error('Generated directory not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(generatedDir);
|
||||||
|
const clientFile = files.find(file => file.includes('openfeature'));
|
||||||
|
if (!clientFile) {
|
||||||
|
throw new Error('openfeature.ts not found');
|
||||||
|
}
|
||||||
|
console.log(`✅ Found: ${clientFile}`);
|
||||||
|
|
||||||
|
|
||||||
|
const clientPath = join(generatedDir, clientFile);
|
||||||
|
|
||||||
|
// 3. Setup OpenFeature provider and test
|
||||||
|
await OpenFeature.setProvider(new TestProvider());
|
||||||
|
|
||||||
|
|
||||||
|
const { getGeneratedClient } = await import(clientPath);
|
||||||
|
const client = getGeneratedClient();
|
||||||
|
|
||||||
|
console.log('🧪 Testing flags...');
|
||||||
|
|
||||||
|
// Test each flag
|
||||||
|
const tests = [
|
||||||
|
{ name: 'enableFeatureA', expected: 'boolean' },
|
||||||
|
{ name: 'greetingMessage', expected: 'string' },
|
||||||
|
{ name: 'usernameMaxLength', expected: 'number' },
|
||||||
|
{ name: 'discountPercentage', expected: 'number' },
|
||||||
|
{ name: 'themeCustomization', expected: 'object' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
if (client[test.name]) {
|
||||||
|
const result = await client[test.name]();
|
||||||
|
const type = typeof result;
|
||||||
|
if (type === test.expected) {
|
||||||
|
console.log(`✅ ${test.name}: ${result} | type: (${type})`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`${test.name} returned ${type}, expected ${test.expected}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ ${test.name} method not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 All tests passed!');
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
Loading…
Reference in New Issue