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:
|
||||
- main
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
# Required: allow read access to the content for analysis.
|
||||
|
@ -28,4 +29,4 @@ jobs:
|
|||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.64
|
||||
only-new-issues: true
|
||||
only-new-issues: true
|
||||
|
|
|
@ -43,4 +43,23 @@ jobs:
|
|||
echo "::error file=Makefile::Doc generation produced diff. Run 'make generate-docs' and commit results."
|
||||
git diff
|
||||
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'
|
||||
|
|
|
@ -26,4 +26,11 @@ go.work.sum
|
|||
dist
|
||||
|
||||
# openfeature cli config
|
||||
.openfeature.yaml
|
||||
.openfeature.yaml
|
||||
|
||||
.idea/
|
||||
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
generated/
|
||||
*.log
|
|
@ -21,6 +21,7 @@ builds:
|
|||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
binary: ./cmd/openfeature
|
||||
|
||||
archives:
|
||||
- formats: tar.gz
|
||||
|
@ -35,49 +36,49 @@ archives:
|
|||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [ 'zip' ]
|
||||
formats: ["zip"]
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
|
||||
report_sizes: true
|
||||
|
||||
dockers:
|
||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- --platform=linux/amd64
|
||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.version={{ .Version }}
|
||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
||||
- --label=org.opencontainers.image.description="OpenFeature’s official command-line tool"
|
||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-amd64"]
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- --platform=linux/amd64
|
||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.version={{ .Version }}
|
||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
||||
- --label=org.opencontainers.image.description="OpenFeature’s official command-line tool"
|
||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- --platform=linux/arm64
|
||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.version={{ .Version }}
|
||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
||||
- --label=org.opencontainers.image.description="OpenFeature’s official command-line tool"
|
||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||
- image_templates: ["ghcr.io/open-feature/cli:{{ .Version }}-arm64"]
|
||||
goarch: arm64
|
||||
dockerfile: Dockerfile
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- --platform=linux/arm64
|
||||
- --label=org.opencontainers.image.title={{ .ProjectName }} cli
|
||||
- --label=org.opencontainers.image.url=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.source=https://github.com/open-feature/cli
|
||||
- --label=org.opencontainers.image.version={{ .Version }}
|
||||
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
|
||||
- --label=org.opencontainers.image.description="OpenFeature’s official command-line tool"
|
||||
- --label=org.opencontainers.image.revision={{ .FullCommit }}
|
||||
- --label=org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
docker_manifests:
|
||||
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
|
||||
image_templates:
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||
- name_template: ghcr.io/open-feature/cli:latest
|
||||
image_templates:
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||
- name_template: ghcr.io/open-feature/cli:{{ .Version }}
|
||||
image_templates:
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||
- name_template: ghcr.io/open-feature/cli:latest
|
||||
image_templates:
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-amd64
|
||||
- ghcr.io/open-feature/cli:{{ .Version }}-arm64
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
".": "0.3.2"
|
||||
".": "0.3.5"
|
||||
}
|
||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,5 +1,40 @@
|
|||
# Changelog
|
||||
|
||||
## [0.3.5](https://github.com/open-feature/cli/compare/v0.3.4...v0.3.5) (2025-05-20)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* Naming of generated java class ([#111](https://github.com/open-feature/cli/issues/111)) ([49e65c8](https://github.com/open-feature/cli/commit/49e65c828330abb732eb3b9cf85850bb5ac36531))
|
||||
|
||||
## [0.3.4](https://github.com/open-feature/cli/compare/v0.3.3...v0.3.4) (2025-05-14)
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add java generator ([#107](https://github.com/open-feature/cli/issues/107)) ([9a9f11f](https://github.com/open-feature/cli/commit/9a9f11fc6c6a8ffa38870e62ac26d9f8f679825b))
|
||||
* adds compare command ([#93](https://github.com/open-feature/cli/issues/93)) ([063cfca](https://github.com/open-feature/cli/commit/063cfca2d79c9f75e181422ec375e300e020e57f))
|
||||
* introduce dagger for integration testing and ci ([#100](https://github.com/open-feature/cli/issues/100)) ([96f4cde](https://github.com/open-feature/cli/commit/96f4cde0f87b8daf70e02c1d4ca3bcec018fee02))
|
||||
|
||||
## [0.3.3](https://github.com/open-feature/cli/compare/v0.3.2...v0.3.3) (2025-04-18)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* use the correct json schema url in init command ([#96](https://github.com/open-feature/cli/issues/96)) ([412a117](https://github.com/open-feature/cli/commit/412a1174b5dfe9ba77e18ec57d5a761711067386))
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* add codegen for NestJS ([#99](https://github.com/open-feature/cli/issues/99)) ([5210429](https://github.com/open-feature/cli/commit/5210429e39c10c91482cb0a0a8b2f4431a0aa182))
|
||||
* **csharp:** added generator and integration tests ([#97](https://github.com/open-feature/cli/issues/97)) ([ae64581](https://github.com/open-feature/cli/commit/ae645813c48b5ef10d8557406e7ab5c96ce3df69))
|
||||
* Python generator ([#95](https://github.com/open-feature/cli/issues/95)) ([1f8f43a](https://github.com/open-feature/cli/commit/1f8f43ae049fcf7c4feba3edaa697329688f7343))
|
||||
|
||||
|
||||
### 🧹 Chore
|
||||
|
||||
* automate project standards before push ([#94](https://github.com/open-feature/cli/issues/94)) ([e32547f](https://github.com/open-feature/cli/commit/e32547f73495a525ed4ef5e2cadd45642d6fb172))
|
||||
|
||||
## [0.3.2](https://github.com/open-feature/cli/compare/v0.3.1...v0.3.2) (2025-04-02)
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
# Contributing to OpenFeature CLI
|
||||
|
||||
Thank you for your interest in contributing to the OpenFeature CLI! This document provides guidelines and instructions to help you get started with contributing to the project. Whether you're fixing a bug, adding a new feature, or improving documentation, your contributions are greatly appreciated.
|
||||
|
||||
## Contributing New Generators
|
||||
|
||||
We welcome contributions for new generators to extend the functionality of the OpenFeature CLI. Below are the steps to contribute a new generator:
|
||||
|
@ -32,6 +36,81 @@ We welcome contributions for new generators to extend the functionality of the O
|
|||
|
||||
11. **Address Feedback**: Be responsive to feedback from the maintainers. Make any necessary changes and update your pull request as needed.
|
||||
|
||||
### Testing
|
||||
|
||||
The OpenFeature CLI includes both unit and integration tests to ensure quality and correctness.
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
Run the unit tests with:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
|
||||
To verify that generated code compiles correctly, the project includes integration tests. The CLI uses a Dagger-based integration testing framework to test code generation for each supported language:
|
||||
|
||||
```bash
|
||||
# Run all integration tests
|
||||
make test-integration
|
||||
|
||||
# Run tests for a specific language
|
||||
make test-csharp-dagger
|
||||
```
|
||||
|
||||
For more information on the integration testing framework, see [Integration Testing](./docs/integration-testing.md).
|
||||
|
||||
## Setting Up Lefthook
|
||||
|
||||
To streamline the setup of Git hooks for this project, we utilize [Lefthook](https://github.com/evilmartians/lefthook). Lefthook automates pre-commit and pre-push checks, ensuring consistent enforcement of best practices across the team. These checks include code formatting, documentation generation, and running tests.
|
||||
|
||||
This tool is particularly helpful for new contributors or those returning to the project after some time, as it provides a seamless way to align with the project's standards. By catching issues early in your local development environment, Lefthook helps you address potential problems before opening a Pull Request, saving time and effort for both contributors and maintainers.
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install Lefthook using Homebrew:
|
||||
|
||||
```bash
|
||||
brew install lefthook
|
||||
```
|
||||
|
||||
2. Install the Lefthook configuration into your Git repository:
|
||||
|
||||
```bash
|
||||
lefthook install
|
||||
```
|
||||
|
||||
### Pre-Commit Hook
|
||||
|
||||
The pre-commit hook is configured to run the following check:
|
||||
|
||||
1. **Code Formatting**: Ensures all files are properly formatted using `go fmt`. Any changes made by `go fmt` will be automatically staged.
|
||||
|
||||
### Pre-Push Hook
|
||||
|
||||
The pre-push hook is configured to run the following checks:
|
||||
|
||||
1. **Documentation Generation**: Runs `make generate-docs` to ensure documentation is up-to-date. If any changes are detected, the push will be blocked until the changes are committed.
|
||||
2. **Tests**: Executes `make test` to verify that all tests pass. If any tests fail, the push will be blocked.
|
||||
|
||||
### Running Hooks Manually
|
||||
|
||||
You can manually run the hooks using the following commands:
|
||||
|
||||
- Pre-commit hook:
|
||||
|
||||
```bash
|
||||
lefthook run pre-commit
|
||||
```
|
||||
|
||||
- Pre-push hook:
|
||||
|
||||
```bash
|
||||
lefthook run pre-push
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
### Data
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.21
|
||||
FROM alpine:3.22
|
||||
|
||||
COPY ./openfeature usr/local/bin/openfeature
|
||||
|
||||
|
|
30
Makefile
30
Makefile
|
@ -1,10 +1,30 @@
|
|||
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@go test -v ./...
|
||||
@echo "Tests passed successfully!"
|
||||
|
||||
# Dagger-based integration tests
|
||||
.PHONY: test-integration-csharp
|
||||
test-integration-csharp:
|
||||
@echo "Running C# integration test with Dagger..."
|
||||
@go run ./test/integration/cmd/csharp/run.go
|
||||
|
||||
.PHONY: test-integration-go
|
||||
test-integration-go:
|
||||
@echo "Running Go integration test with Dagger..."
|
||||
@go run ./test/integration/cmd/go/run.go
|
||||
|
||||
.PHONY: test-integration-nodejs
|
||||
test-integration-nodejs:
|
||||
@echo "Running NodeJS integration test with Dagger..."
|
||||
@go run ./test/integration/cmd/nodejs/run.go
|
||||
|
||||
.PHONY: test-integration
|
||||
test-integration:
|
||||
@echo "Running all integration tests with Dagger..."
|
||||
@go run ./test/integration/cmd/run.go
|
||||
|
||||
generate-docs:
|
||||
@echo "Generating documentation..."
|
||||
@go run ./docs/generate-commands.go
|
||||
|
@ -13,4 +33,10 @@ generate-docs:
|
|||
generate-schema:
|
||||
@echo "Generating schema..."
|
||||
@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:
|
||||
|
||||
```bash
|
||||
go install github.com/open-feature/cli@latest
|
||||
go install github.com/open-feature/cli/cmd/openfeature@latest
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
import "github.com/open-feature/cli/cmd"
|
||||
import "github.com/open-feature/cli/internal/cmd"
|
||||
|
||||
var (
|
||||
// Overridden by Go Releaser at build time
|
|
@ -23,6 +23,7 @@ openfeature [flags]
|
|||
|
||||
### SEE ALSO
|
||||
|
||||
* [openfeature compare](openfeature_compare.md) - Compare two feature flag manifests
|
||||
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
|
||||
* [openfeature init](openfeature_init.md) - Initialize a new project
|
||||
* [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI
|
||||
|
|
|
@ -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
|
||||
|
||||
* [openfeature](openfeature.md) - CLI for OpenFeature.
|
||||
* [openfeature generate csharp](openfeature_generate_csharp.md) - Generate typesafe C# client.
|
||||
* [openfeature generate go](openfeature_generate_go.md) - Generate typesafe accessors for OpenFeature.
|
||||
* [openfeature generate java](openfeature_generate_java.md) - Generate typesafe Java client.
|
||||
* [openfeature generate nestjs](openfeature_generate_nestjs.md) - Generate typesafe NestJS decorators.
|
||||
* [openfeature generate nodejs](openfeature_generate_nodejs.md) - Generate typesafe Node.js client.
|
||||
* [openfeature generate python](openfeature_generate_python.md) - Generate typesafe Python client.
|
||||
* [openfeature generate react](openfeature_generate_react.md) - Generate typesafe React Hooks.
|
||||
|
||||
|
|
|
@ -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"
|
||||
"strings"
|
||||
|
||||
"github.com/open-feature/cli/cmd"
|
||||
"github.com/open-feature/cli/internal/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
|
70
go.mod
70
go.mod
|
@ -2,53 +2,85 @@ module github.com/open-feature/cli
|
|||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
dagger.io/dagger v0.18.12
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/pterm/pterm v0.12.80
|
||||
github.com/pterm/pterm v0.12.81
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/spf13/viper v1.20.0
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/text v0.23.0
|
||||
golang.org/x/text v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
github.com/99designs/gqlgen v0.17.75 // indirect
|
||||
github.com/Khan/genqlient v0.8.1 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.8.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.28 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
|
161
go.sum
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/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
dagger.io/dagger v0.18.10 h1:Ibyz5LqxjjEHfLMlaU9PJ3xt3ju7p29RWy0lVfvSNU0=
|
||||
dagger.io/dagger v0.18.10/go.mod h1:VSj+2HMd/EnaCVt7gTY70p8LBW+oQDYjA1XTadr8vBE=
|
||||
dagger.io/dagger v0.18.11 h1:6lSfemlbGM2HmdOjhgevrX2+orMDGKU/xTaBMZ+otyY=
|
||||
dagger.io/dagger v0.18.11/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
|
||||
dagger.io/dagger v0.18.12 h1:s7v8aHlzDUogZ/jW92lHC+gljCNRML+0mosfh13R4vs=
|
||||
dagger.io/dagger v0.18.12/go.mod h1:azlZ24m2br95t0jQHUBpL5SiafeqtVDLl1Itlq6GO+4=
|
||||
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
|
||||
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
|
||||
github.com/99designs/gqlgen v0.17.75 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI=
|
||||
github.com/99designs/gqlgen v0.17.75/go.mod h1:p7gbTpdnHyl70hmSpM8XG8GiKwmCv+T5zkdY8U8bLog=
|
||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
|
@ -15,38 +27,56 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
|
|||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
|
@ -61,13 +91,15 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -78,32 +110,34 @@ github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEej
|
|||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
|
||||
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
|
||||
github.com/pterm/pterm v0.12.81 h1:ju+j5I2++FO1jBKMmscgh5h5DPFDFMB7epEjSoKehKA=
|
||||
github.com/pterm/pterm v0.12.81/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
|
||||
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
|
||||
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
|
@ -113,10 +147,15 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8xLRkY=
|
||||
github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
|
@ -125,21 +164,61 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
|
|||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
||||
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6 h1:gllJVKwONftmCc4KlNbN8o/LvmbxotqQy6zzi6yDQOQ=
|
||||
golang.org/x/exp v0.0.0-20250530174510-65e920069ea6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -149,31 +228,41 @@ golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
|||
// Set the config file name and path
|
||||
v.SetConfigName(".openfeature")
|
||||
v.AddConfigPath(".")
|
||||
|
||||
|
||||
logger.Default.Debug("Looking for .openfeature config file in current directory")
|
||||
|
||||
// Read the config file
|
||||
|
@ -31,7 +31,6 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
|||
} else {
|
||||
logger.Default.Debug(fmt.Sprintf("Using config file: %s", v.ConfigFileUsed()))
|
||||
}
|
||||
|
||||
|
||||
// Track which flags were set directly via command line
|
||||
cmdLineFlags := make(map[string]bool)
|
||||
|
@ -50,11 +49,11 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
|||
|
||||
// Build configuration paths from most specific to least specific
|
||||
configPaths := []string{}
|
||||
|
||||
|
||||
// Check the most specific path (e.g., generate.go.package-name)
|
||||
if bindPrefix != "" {
|
||||
configPaths = append(configPaths, bindPrefix + "." + f.Name)
|
||||
|
||||
configPaths = append(configPaths, bindPrefix+"."+f.Name)
|
||||
|
||||
// Check parent paths (e.g., generate.package-name)
|
||||
parts := strings.Split(bindPrefix, ".")
|
||||
for i := len(parts) - 1; i > 0; i-- {
|
||||
|
@ -62,12 +61,12 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
|||
configPaths = append(configPaths, parentPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check the base path (e.g., package-name)
|
||||
configPaths = append(configPaths, f.Name)
|
||||
|
||||
|
||||
logger.Default.Debug(fmt.Sprintf("Looking for config value for flag %s in paths: %s", f.Name, strings.Join(configPaths, ", ")))
|
||||
|
||||
|
||||
// Try each path in order until we find a match
|
||||
for _, path := range configPaths {
|
||||
if v.IsSet(path) {
|
||||
|
@ -81,7 +80,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Log the final value for the flag
|
||||
logger.Default.Debug(fmt.Sprintf("Final flag value: %s=%s", f.Name, f.Value.String()))
|
||||
})
|
|
@ -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
|
||||
outputPath string // output directory (optional, defaults to "output")
|
||||
outputFile string // output file name
|
||||
packageName string // optional, only used for Go
|
||||
packageName string // optional, used for Go (package-name), Java (package-name) and C# (namespace)
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
|
@ -48,6 +48,36 @@ func TestGenerate(t *testing.T) {
|
|||
outputGolden: "testdata/success_nodejs.golden",
|
||||
outputFile: "openfeature.ts",
|
||||
},
|
||||
{
|
||||
name: "NestJS generation success",
|
||||
command: "nestjs",
|
||||
manifestGolden: "testdata/success_manifest.golden",
|
||||
outputGolden: "testdata/success_nestjs.golden",
|
||||
outputFile: "openfeature-decorators.ts",
|
||||
},
|
||||
{
|
||||
name: "Python generation success",
|
||||
command: "python",
|
||||
manifestGolden: "testdata/success_manifest.golden",
|
||||
outputGolden: "testdata/success_python.golden",
|
||||
outputFile: "openfeature.py",
|
||||
},
|
||||
{
|
||||
name: "CSharp generation success",
|
||||
command: "csharp",
|
||||
manifestGolden: "testdata/success_manifest.golden",
|
||||
outputGolden: "testdata/success_csharp.golden",
|
||||
outputFile: "OpenFeature.g.cs",
|
||||
packageName: "TestNamespace", // Using packageName field for namespace
|
||||
},
|
||||
{
|
||||
name: "Java generation success",
|
||||
command: "java",
|
||||
manifestGolden: "testdata/success_manifest.golden",
|
||||
outputGolden: "testdata/success_java.golden",
|
||||
outputFile: "OpenFeature.java",
|
||||
packageName: "com.example.openfeature",
|
||||
},
|
||||
// Add more test cases here as needed
|
||||
}
|
||||
|
||||
|
@ -60,7 +90,7 @@ func TestGenerate(t *testing.T) {
|
|||
|
||||
// Constant paths
|
||||
const memoryManifestPath = "manifest/path.json"
|
||||
|
||||
|
||||
// Use default output path if not specified
|
||||
outputPath := tc.outputPath
|
||||
if outputPath == "" {
|
||||
|
@ -79,9 +109,15 @@ func TestGenerate(t *testing.T) {
|
|||
"--output", outputPath,
|
||||
}
|
||||
|
||||
// Add package name if provided (for Go)
|
||||
// Add parameters specific to each generator
|
||||
if tc.packageName != "" {
|
||||
args = append(args, "--package-name", tc.packageName)
|
||||
if tc.command == "csharp" {
|
||||
args = append(args, "--namespace", tc.packageName)
|
||||
} else if tc.command == "go" {
|
||||
args = append(args, "--package-name", tc.packageName)
|
||||
} else if tc.command == "java" {
|
||||
args = append(args, "--package-name", tc.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
cmd.SetArgs(args)
|
||||
|
@ -100,7 +136,7 @@ func TestGenerate(t *testing.T) {
|
|||
|
||||
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
|
||||
data, err := os.ReadFile(inputPath)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
t.Fatalf("error reading file %q: %v", inputPath, err)
|
||||
}
|
||||
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
|
||||
|
@ -120,6 +156,17 @@ func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string,
|
|||
}
|
||||
}
|
||||
|
||||
// normalizeLines trims trailing whitespace and carriage returns from each line.
|
||||
// This helps ensure consistent comparison by ignoring formatting differences like indentation or line endings.
|
||||
func normalizeLines(input []string) []string {
|
||||
normalized := make([]string, len(input))
|
||||
for i, line := range input {
|
||||
// Trim right whitespace and convert \r\n or \r to \n
|
||||
normalized[i] = strings.TrimRight(line, " \t\r")
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
|
||||
want, err := os.ReadFile(testFile)
|
||||
if err != nil {
|
||||
|
@ -130,11 +177,11 @@ func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs)
|
|||
if err != nil {
|
||||
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
|
||||
}
|
||||
|
||||
|
||||
// Convert to string arrays by splitting on newlines
|
||||
wantLines := strings.Split(string(want), "\n")
|
||||
gotLines := strings.Split(string(got), "\n")
|
||||
|
||||
wantLines := normalizeLines(strings.Split(string(want), "\n"))
|
||||
gotLines := normalizeLines(strings.Split(string(got), "\n"))
|
||||
|
||||
if diff := cmp.Diff(wantLines, gotLines); diff != "" {
|
||||
t.Errorf("output mismatch (-want +got):\n%s", diff)
|
||||
}
|
|
@ -34,7 +34,7 @@ func GetInitCmd() *cobra.Command {
|
|||
logger.Default.Info("No changes were made.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
logger.Default.Debug("User confirmed override of existing manifest")
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ func GetInitCmd() *cobra.Command {
|
|||
logger.Default.Error(fmt.Sprintf("Failed to create manifest: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
logger.Default.FileCreated(manifestPath)
|
||||
logger.Default.Success("Project initialized.")
|
||||
return nil
|
|
@ -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 {
|
||||
printBanner()
|
||||
logger.Default.Println("");
|
||||
logger.Default.Println("")
|
||||
logger.Default.Println("To see all the options, try 'openfeature --help'")
|
||||
return nil
|
||||
},
|
||||
|
@ -62,6 +62,7 @@ func GetRootCmd() *cobra.Command {
|
|||
rootCmd.AddCommand(GetVersionCmd())
|
||||
rootCmd.AddCommand(GetInitCmd())
|
||||
rootCmd.AddCommand(GetGenerateCmd())
|
||||
rootCmd.AddCommand(GetCompareCmd())
|
||||
|
||||
// Add a custom error handler after the command is created
|
||||
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
|
|
@ -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 StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error)
|
||||
type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error)
|
||||
type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error)
|
||||
type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error)
|
||||
|
||||
var client openfeature.IClient = nil
|
||||
// Discount percentage applied to purchases.
|
||||
|
@ -67,6 +69,23 @@ var GreetingMessage = struct {
|
|||
return client.StringValueDetails(ctx, "greetingMessage", "Hello there!", evalCtx)
|
||||
},
|
||||
}
|
||||
// Allows customization of theme colors.
|
||||
var ThemeCustomization = struct {
|
||||
// Value returns the value of the flag ThemeCustomization,
|
||||
// as well as the evaluation error, if present.
|
||||
Value ObjectProvider
|
||||
|
||||
// ValueWithDetails returns the value of the flag ThemeCustomization,
|
||||
// the evaluation error, if any, and the evaluation details.
|
||||
ValueWithDetails ObjectProviderDetails
|
||||
}{
|
||||
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error) {
|
||||
return client.ObjectValue(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
|
||||
},
|
||||
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error){
|
||||
return client.ObjectValueDetails(ctx, "themeCustomization", map[string]any{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}, evalCtx)
|
||||
},
|
||||
}
|
||||
// Maximum allowed length for usernames.
|
||||
var UsernameMaxLength = struct {
|
||||
// Value returns the value of the flag UsernameMaxLength,
|
|
@ -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,
|
||||
stringOrUndefined,
|
||||
objectOrUndefined,
|
||||
JsonValue,
|
||||
} from "@openfeature/server-sdk";
|
||||
import type {
|
||||
EvaluationContext,
|
||||
|
@ -101,6 +102,36 @@ export interface GeneratedClient {
|
|||
*/
|
||||
greetingMessageDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<string>>;
|
||||
|
||||
/**
|
||||
* Allows customization of theme colors.
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `themeCustomization`
|
||||
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
|
||||
* - type: `JsonValue`
|
||||
*
|
||||
* Performs a flag evaluation that returns a object.
|
||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||
* @returns {Promise<JsonValue>} Flag evaluation response
|
||||
*/
|
||||
themeCustomization(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<JsonValue>;
|
||||
|
||||
/**
|
||||
* Allows customization of theme colors.
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `themeCustomization`
|
||||
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
|
||||
* - type: `JsonValue`
|
||||
*
|
||||
* Performs a flag evaluation that a returns an evaluation details object.
|
||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||
* @returns {Promise<EvaluationDetails<JsonValue>>} Flag evaluation details response
|
||||
*/
|
||||
themeCustomizationDetails(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<JsonValue>>;
|
||||
|
||||
/**
|
||||
* Maximum allowed length for usernames.
|
||||
*
|
||||
|
@ -185,6 +216,14 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
|
|||
return client.getStringDetails("greetingMessage", "Hello there!", context, options);
|
||||
},
|
||||
|
||||
themeCustomization: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<JsonValue> => {
|
||||
return client.getObjectValue("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options);
|
||||
},
|
||||
|
||||
themeCustomizationDetails: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<JsonValue>> => {
|
||||
return client.getObjectDetails("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, context, options);
|
||||
},
|
||||
|
||||
usernameMaxLength: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<number> => {
|
||||
return client.getNumberValue("usernameMaxLength", 50, context, options);
|
||||
},
|
|
@ -0,0 +1,472 @@
|
|||
# AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
|
||||
from typing import Optional
|
||||
|
||||
from openfeature.client import OpenFeatureClient
|
||||
from openfeature.evaluation_context import EvaluationContext
|
||||
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagEvaluationOptions
|
||||
from openfeature.hook import Hook
|
||||
|
||||
|
||||
class GeneratedClient:
|
||||
def __init__(
|
||||
self,
|
||||
client: OpenFeatureClient,
|
||||
) -> None:
|
||||
self.client = client
|
||||
|
||||
def discount_percentage(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> float:
|
||||
"""
|
||||
Discount percentage applied to purchases.
|
||||
|
||||
**Details:**
|
||||
- flag key: `discountPercentage`
|
||||
- default value: `0.15`
|
||||
- type: `float`
|
||||
|
||||
Performs a flag evaluation that returns a `float`.
|
||||
"""
|
||||
return self.client.get_float_value(
|
||||
flag_key="discountPercentage",
|
||||
default_value=0.15,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def discount_percentage_details(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Discount percentage applied to purchases.
|
||||
|
||||
**Details:**
|
||||
- flag key: `discountPercentage`
|
||||
- default value: `0.15`
|
||||
- type: `float`
|
||||
|
||||
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return self.client.get_float_details(
|
||||
flag_key="discountPercentage",
|
||||
default_value=0.15,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def discount_percentage_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> float:
|
||||
"""
|
||||
Discount percentage applied to purchases.
|
||||
|
||||
**Details:**
|
||||
- flag key: `discountPercentage`
|
||||
- default value: `0.15`
|
||||
- type: `float`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `float`.
|
||||
"""
|
||||
return await self.client.get_float_value_async(
|
||||
flag_key="discountPercentage",
|
||||
default_value=0.15,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def discount_percentage_details_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Discount percentage applied to purchases.
|
||||
|
||||
**Details:**
|
||||
- flag key: `discountPercentage`
|
||||
- default value: `0.15`
|
||||
- type: `float`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return await self.client.get_float_details_async(
|
||||
flag_key="discountPercentage",
|
||||
default_value=0.15,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def enable_feature_a(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Controls whether Feature A is enabled.
|
||||
|
||||
**Details:**
|
||||
- flag key: `enableFeatureA`
|
||||
- default value: `False`
|
||||
- type: `bool`
|
||||
|
||||
Performs a flag evaluation that returns a `bool`.
|
||||
"""
|
||||
return self.client.get_boolean_value(
|
||||
flag_key="enableFeatureA",
|
||||
default_value=False,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def enable_feature_a_details(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Controls whether Feature A is enabled.
|
||||
|
||||
**Details:**
|
||||
- flag key: `enableFeatureA`
|
||||
- default value: `False`
|
||||
- type: `bool`
|
||||
|
||||
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return self.client.get_boolean_details(
|
||||
flag_key="enableFeatureA",
|
||||
default_value=False,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def enable_feature_a_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Controls whether Feature A is enabled.
|
||||
|
||||
**Details:**
|
||||
- flag key: `enableFeatureA`
|
||||
- default value: `False`
|
||||
- type: `bool`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `bool`.
|
||||
"""
|
||||
return await self.client.get_boolean_value_async(
|
||||
flag_key="enableFeatureA",
|
||||
default_value=False,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def enable_feature_a_details_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Controls whether Feature A is enabled.
|
||||
|
||||
**Details:**
|
||||
- flag key: `enableFeatureA`
|
||||
- default value: `False`
|
||||
- type: `bool`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return await self.client.get_boolean_details_async(
|
||||
flag_key="enableFeatureA",
|
||||
default_value=False,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def greeting_message(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> str:
|
||||
"""
|
||||
The message to use for greeting users.
|
||||
|
||||
**Details:**
|
||||
- flag key: `greetingMessage`
|
||||
- default value: `Hello there!`
|
||||
- type: `str`
|
||||
|
||||
Performs a flag evaluation that returns a `str`.
|
||||
"""
|
||||
return self.client.get_string_value(
|
||||
flag_key="greetingMessage",
|
||||
default_value="Hello there!",
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def greeting_message_details(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
The message to use for greeting users.
|
||||
|
||||
**Details:**
|
||||
- flag key: `greetingMessage`
|
||||
- default value: `Hello there!`
|
||||
- type: `str`
|
||||
|
||||
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return self.client.get_string_details(
|
||||
flag_key="greetingMessage",
|
||||
default_value="Hello there!",
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def greeting_message_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> str:
|
||||
"""
|
||||
The message to use for greeting users.
|
||||
|
||||
**Details:**
|
||||
- flag key: `greetingMessage`
|
||||
- default value: `Hello there!`
|
||||
- type: `str`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `str`.
|
||||
"""
|
||||
return await self.client.get_string_value_async(
|
||||
flag_key="greetingMessage",
|
||||
default_value="Hello there!",
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def greeting_message_details_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
The message to use for greeting users.
|
||||
|
||||
**Details:**
|
||||
- flag key: `greetingMessage`
|
||||
- default value: `Hello there!`
|
||||
- type: `str`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return await self.client.get_string_details_async(
|
||||
flag_key="greetingMessage",
|
||||
default_value="Hello there!",
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def theme_customization(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> object:
|
||||
"""
|
||||
Allows customization of theme colors.
|
||||
|
||||
**Details:**
|
||||
- flag key: `themeCustomization`
|
||||
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
|
||||
- type: `object`
|
||||
|
||||
Performs a flag evaluation that returns a `object`.
|
||||
"""
|
||||
return self.client.get_object_value(
|
||||
flag_key="themeCustomization",
|
||||
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def theme_customization_details(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Allows customization of theme colors.
|
||||
|
||||
**Details:**
|
||||
- flag key: `themeCustomization`
|
||||
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
|
||||
- type: `object`
|
||||
|
||||
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return self.client.get_object_details(
|
||||
flag_key="themeCustomization",
|
||||
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def theme_customization_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> object:
|
||||
"""
|
||||
Allows customization of theme colors.
|
||||
|
||||
**Details:**
|
||||
- flag key: `themeCustomization`
|
||||
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
|
||||
- type: `object`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `object`.
|
||||
"""
|
||||
return await self.client.get_object_value_async(
|
||||
flag_key="themeCustomization",
|
||||
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def theme_customization_details_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Allows customization of theme colors.
|
||||
|
||||
**Details:**
|
||||
- flag key: `themeCustomization`
|
||||
- default value: `{"primaryColor": "#007bff", "secondaryColor": "#6c757d"}`
|
||||
- type: `object`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return await self.client.get_object_details_async(
|
||||
flag_key="themeCustomization",
|
||||
default_value={"primaryColor": "#007bff", "secondaryColor": "#6c757d"},
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def username_max_length(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Maximum allowed length for usernames.
|
||||
|
||||
**Details:**
|
||||
- flag key: `usernameMaxLength`
|
||||
- default value: `50`
|
||||
- type: `int`
|
||||
|
||||
Performs a flag evaluation that returns a `int`.
|
||||
"""
|
||||
return self.client.get_integer_value(
|
||||
flag_key="usernameMaxLength",
|
||||
default_value=50,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
def username_max_length_details(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Maximum allowed length for usernames.
|
||||
|
||||
**Details:**
|
||||
- flag key: `usernameMaxLength`
|
||||
- default value: `50`
|
||||
- type: `int`
|
||||
|
||||
Performs a flag evaluation that returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return self.client.get_integer_details(
|
||||
flag_key="usernameMaxLength",
|
||||
default_value=50,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def username_max_length_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Maximum allowed length for usernames.
|
||||
|
||||
**Details:**
|
||||
- flag key: `usernameMaxLength`
|
||||
- default value: `50`
|
||||
- type: `int`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `int`.
|
||||
"""
|
||||
return await self.client.get_integer_value_async(
|
||||
flag_key="usernameMaxLength",
|
||||
default_value=50,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
async def username_max_length_details_async(
|
||||
self,
|
||||
evaluation_context: Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Maximum allowed length for usernames.
|
||||
|
||||
**Details:**
|
||||
- flag key: `usernameMaxLength`
|
||||
- default value: `50`
|
||||
- type: `int`
|
||||
|
||||
Performs a flag evaluation asynchronously and returns a `FlagEvaluationDetails` instance.
|
||||
"""
|
||||
return await self.client.get_integer_details_async(
|
||||
flag_key="usernameMaxLength",
|
||||
default_value=50,
|
||||
evaluation_context=evaluation_context,
|
||||
flag_evaluation_options=flag_evaluation_options,
|
||||
)
|
||||
|
||||
|
||||
def get_generated_client(
|
||||
client: Optional[OpenFeatureClient] = None,
|
||||
domain: Optional[str] = None,
|
||||
version: Optional[str] = None,
|
||||
context: Optional[EvaluationContext] = None,
|
||||
hooks: Optional[list[Hook]] = None,
|
||||
) -> GeneratedClient:
|
||||
if not client:
|
||||
client = OpenFeatureClient(
|
||||
domain=domain,
|
||||
version=version,
|
||||
context=context,
|
||||
hooks=hooks,
|
||||
)
|
||||
return GeneratedClient(client)
|
|
@ -5,6 +5,7 @@ import {
|
|||
type ReactFlagEvaluationNoSuspenseOptions,
|
||||
useFlag,
|
||||
useSuspenseFlag,
|
||||
JsonValue
|
||||
} from "@openfeature/react-sdk";
|
||||
|
||||
/**
|
||||
|
@ -88,6 +89,33 @@ export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspen
|
|||
return useSuspenseFlag("greetingMessage", "Hello there!", options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows customization of theme colors.
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `themeCustomization`
|
||||
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
|
||||
* - type: `JsonValue`
|
||||
*/
|
||||
export const useThemeCustomization = (options?: ReactFlagEvaluationOptions) => {
|
||||
return useFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows customization of theme colors.
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `themeCustomization`
|
||||
* - default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}`
|
||||
* - type: `JsonValue`
|
||||
*
|
||||
* Equivalent to useFlag with options: `{ suspend: true }`
|
||||
* @experimental — Suspense is an experimental feature subject to change in future versions.
|
||||
*/
|
||||
export const useSuspenseThemeCustomization = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
|
||||
return useSuspenseFlag("themeCustomization", {"primaryColor":"#007bff","secondaryColor":"#6c757d"}, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximum allowed length for usernames.
|
||||
*
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,4 +17,4 @@ func printBanner() {
|
|||
pterm.Println()
|
||||
pterm.Printf("version: %s | compiled: %s\n", pterm.LightGreen(Version), pterm.LightGreen(Date))
|
||||
pterm.Println(pterm.Cyan("🔗 https://openfeature.dev | https://github.com/open-feature/cli"))
|
||||
}
|
||||
}
|
|
@ -38,4 +38,4 @@ func GetVersionCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
return versionCmd
|
||||
}
|
||||
}
|
|
@ -6,19 +6,23 @@ import (
|
|||
|
||||
// Flag name constants to avoid duplication
|
||||
const (
|
||||
DebugFlagName = "debug"
|
||||
ManifestFlagName = "manifest"
|
||||
OutputFlagName = "output"
|
||||
NoInputFlagName = "no-input"
|
||||
GoPackageFlagName = "package-name"
|
||||
OverrideFlagName = "override"
|
||||
DebugFlagName = "debug"
|
||||
ManifestFlagName = "manifest"
|
||||
OutputFlagName = "output"
|
||||
NoInputFlagName = "no-input"
|
||||
GoPackageFlagName = "package-name"
|
||||
CSharpNamespaceName = "namespace"
|
||||
OverrideFlagName = "override"
|
||||
JavaPackageFlagName = "package-name"
|
||||
)
|
||||
|
||||
// Default values for flags
|
||||
const (
|
||||
DefaultManifestPath = "flags.json"
|
||||
DefaultOutputPath = ""
|
||||
DefaultGoPackageName = "openfeature"
|
||||
DefaultManifestPath = "flags.json"
|
||||
DefaultOutputPath = ""
|
||||
DefaultGoPackageName = "openfeature"
|
||||
DefaultCSharpNamespace = "OpenFeature"
|
||||
DefaultJavaPackageName = "com.example.openfeature"
|
||||
)
|
||||
|
||||
// AddRootFlags adds the common flags to the given command
|
||||
|
@ -38,6 +42,16 @@ func AddGoGenerateFlags(cmd *cobra.Command) {
|
|||
cmd.Flags().String(GoPackageFlagName, DefaultGoPackageName, "Name of the generated Go package")
|
||||
}
|
||||
|
||||
// AddCSharpGenerateFlags adds the C# generator specific flags to the given command
|
||||
func AddCSharpGenerateFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String(CSharpNamespaceName, DefaultCSharpNamespace, "Namespace for the generated C# code")
|
||||
}
|
||||
|
||||
// AddJavaGenerateFlags adds the Java generator specific flags to the given command
|
||||
func AddJavaGenerateFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String(JavaPackageFlagName, DefaultJavaPackageName, "Name of the generated Java package")
|
||||
}
|
||||
|
||||
// AddInitFlags adds the init command specific flags
|
||||
func AddInitFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
|
||||
|
@ -61,6 +75,18 @@ func GetGoPackageName(cmd *cobra.Command) string {
|
|||
return goPackageName
|
||||
}
|
||||
|
||||
// GetCSharpNamespace gets the C# namespace from the given command
|
||||
func GetCSharpNamespace(cmd *cobra.Command) string {
|
||||
namespace, _ := cmd.Flags().GetString(CSharpNamespaceName)
|
||||
return namespace
|
||||
}
|
||||
|
||||
// GetJavaPackageName gets the Java package name from the given command
|
||||
func GetJavaPackageName(cmd *cobra.Command) string {
|
||||
javaPackageName, _ := cmd.Flags().GetString(JavaPackageFlagName)
|
||||
return javaPackageName
|
||||
}
|
||||
|
||||
// GetNoInput gets the no-input flag from the given command
|
||||
func GetNoInput(cmd *cobra.Command) bool {
|
||||
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/open-feature/cli/internal/filesystem"
|
||||
"github.com/open-feature/cli/internal/manifest"
|
||||
|
@ -27,24 +28,24 @@ const (
|
|||
func (f FlagType) String() string {
|
||||
switch f {
|
||||
case IntType:
|
||||
return "int"
|
||||
return "int"
|
||||
case FloatType:
|
||||
return "float"
|
||||
return "float"
|
||||
case BoolType:
|
||||
return "bool"
|
||||
return "bool"
|
||||
case StringType:
|
||||
return "string"
|
||||
return "string"
|
||||
case ObjectType:
|
||||
return "object"
|
||||
return "object"
|
||||
default:
|
||||
return "unknown"
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Flag struct {
|
||||
Key string
|
||||
Type FlagType
|
||||
Description string
|
||||
Key string
|
||||
Type FlagType
|
||||
Description string
|
||||
DefaultValue any
|
||||
}
|
||||
|
||||
|
@ -64,7 +65,7 @@ func Load(manifestPath string) (*Flagset, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(validationErrors) > 0 {
|
||||
return nil, fmt.Errorf("validation failed: %v", validationErrors)
|
||||
return nil, errors.New(FormatValidationError(validationErrors))
|
||||
}
|
||||
|
||||
var flagset Flagset
|
||||
|
@ -90,9 +91,9 @@ func (fs *Flagset) Filter(unsupportedFlagTypes map[FlagType]bool) *Flagset {
|
|||
func (fs *Flagset) UnmarshalJSON(data []byte) error {
|
||||
var manifest struct {
|
||||
Flags map[string]struct {
|
||||
FlagType string `json:"flagType"`
|
||||
Description string `json:"description"`
|
||||
DefaultValue any `json:"defaultValue"`
|
||||
FlagType string `json:"flagType"`
|
||||
Description string `json:"description"`
|
||||
DefaultValue any `json:"defaultValue"`
|
||||
} `json:"flags"`
|
||||
}
|
||||
|
||||
|
@ -131,4 +132,44 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
|
|||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func FormatValidationError(issues []manifest.ValidationError) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("flag manifest validation failed:\n\n")
|
||||
|
||||
// Group messages by flag path
|
||||
grouped := make(map[string]struct {
|
||||
flagType string
|
||||
messages []string
|
||||
})
|
||||
|
||||
for _, issue := range issues {
|
||||
entry := grouped[issue.Path]
|
||||
entry.flagType = issue.Type
|
||||
entry.messages = append(entry.messages, issue.Message)
|
||||
grouped[issue.Path] = entry
|
||||
}
|
||||
|
||||
// Sort paths for consistent output
|
||||
paths := make([]string, 0, len(grouped))
|
||||
for path := range grouped {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
|
||||
// Format each row
|
||||
for _, path := range paths {
|
||||
entry := grouped[path]
|
||||
flagType := entry.flagType
|
||||
if flagType == "" {
|
||||
flagType = "missing"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(
|
||||
"- flagType: %s\n flagPath: %s\n errors:\n ~ %s\n \tSuggestions:\n \t- flagType: boolean\n \t- defaultValue: true\n\n",
|
||||
flagType,
|
||||
path,
|
||||
strings.Join(entry.messages, "\n ~ "),
|
||||
))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
// Remapping ToLowerCamel to ToCamel to match the expected behavior
|
||||
// Ref: See above
|
||||
"ToCamel": strcase.ToLowerCamel,
|
||||
"ToKebab": strcase.ToKebab,
|
||||
"ToCamel": strcase.ToLowerCamel,
|
||||
"ToKebab": strcase.ToKebab,
|
||||
"ToScreamingKebab": strcase.ToScreamingKebab,
|
||||
"ToSnake": strcase.ToSnake,
|
||||
"ToSnake": strcase.ToSnake,
|
||||
"ToScreamingSnake": strcase.ToScreamingSnake,
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": cases.Title,
|
||||
"Quote": strconv.Quote,
|
||||
"QuoteString": func (input any) any {
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": cases.Title,
|
||||
"Quote": strconv.Quote,
|
||||
"QuoteString": func(input any) any {
|
||||
if str, ok := input.(string); ok {
|
||||
return strconv.Quote(str)
|
||||
}
|
||||
|
@ -39,4 +39,4 @@ func init() {
|
|||
// results in "Api" using ToCamel("API")
|
||||
// results in "api" using ToLowerCamel("API")
|
||||
strcase.ConfigureAcronym("API", "api")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ func (g *CommonGenerator) GenerateFile(customFunc template.FuncMap, tmpl string,
|
|||
maps.Copy(funcs, customFunc)
|
||||
|
||||
logger.Default.Debug(fmt.Sprintf("Generating file: %s", name))
|
||||
|
||||
|
||||
generatorTemplate, err := template.New("generator").Funcs(funcs).Parse(tmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing template: %v", err)
|
||||
|
@ -68,9 +68,9 @@ func (g *CommonGenerator) GenerateFile(customFunc template.FuncMap, tmpl string,
|
|||
logger.Default.FileFailed(fullPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Log successful file creation
|
||||
logger.Default.FileCreated(fullPath)
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,12 @@ package golang
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/flagset"
|
||||
|
@ -30,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string {
|
|||
return "Boolean"
|
||||
case flagset.StringType:
|
||||
return "String"
|
||||
case flagset.ObjectType:
|
||||
return "Object"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -45,6 +52,8 @@ func typeString(flagType flagset.FlagType) string {
|
|||
return "bool"
|
||||
case flagset.FloatType:
|
||||
return "float64"
|
||||
case flagset.ObjectType:
|
||||
return "map[string]any"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -60,11 +69,68 @@ func supportImports(flags []flagset.Flag) []string {
|
|||
return res
|
||||
}
|
||||
|
||||
func toMapLiteral(value any) string {
|
||||
assertedMap, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
// To have a determined order of the object for comparison
|
||||
keys := slices.Sorted(maps.Keys(assertedMap))
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("map[string]any{")
|
||||
|
||||
for index, key := range keys {
|
||||
if index > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
val := assertedMap[key]
|
||||
|
||||
builder.WriteString(fmt.Sprintf(`%q: %s`, key, formatNestedValue(val)))
|
||||
}
|
||||
|
||||
builder.WriteString("}")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func formatNestedValue(value any) string {
|
||||
switch val := value.(type) {
|
||||
case string:
|
||||
return fmt.Sprintf("%q", val)
|
||||
case bool:
|
||||
return fmt.Sprintf("%t", val)
|
||||
case int, int64, float64:
|
||||
return fmt.Sprintf("%v", val)
|
||||
case map[string]any:
|
||||
return toMapLiteral(val)
|
||||
case []any:
|
||||
var sliceBuilder strings.Builder
|
||||
sliceBuilder.WriteString("[]any{")
|
||||
for index, elem := range val {
|
||||
if index > 0 {
|
||||
sliceBuilder.WriteString(", ")
|
||||
}
|
||||
|
||||
sliceBuilder.WriteString(formatNestedValue(elem))
|
||||
}
|
||||
sliceBuilder.WriteString("}")
|
||||
return sliceBuilder.String()
|
||||
default:
|
||||
jsonBytes, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return "nil"
|
||||
}
|
||||
return fmt.Sprintf("%q", string(jsonBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
|
||||
funcs := template.FuncMap{
|
||||
"SupportImports": supportImports,
|
||||
"OpenFeatureType": openFeatureType,
|
||||
"TypeString": typeString,
|
||||
"ToMapLiteral": toMapLiteral,
|
||||
}
|
||||
|
||||
newParams := &generators.Params[any]{
|
||||
|
@ -80,8 +146,6 @@ func (g *GolangGenerator) Generate(params *generators.Params[Params]) error {
|
|||
// NewGenerator creates a generator for Go.
|
||||
func NewGenerator(fs *flagset.Flagset) *GolangGenerator {
|
||||
return &GolangGenerator{
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
||||
flagset.ObjectType: true,
|
||||
}),
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ type IntProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext
|
|||
type IntProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.IntEvaluationDetails, error)
|
||||
type StringProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (string, error)
|
||||
type StringProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.StringEvaluationDetails, error)
|
||||
type ObjectProvider func(ctx context.Context, evalCtx openfeature.EvaluationContext) (any, error)
|
||||
type ObjectProviderDetails func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.InterfaceEvaluationDetails, error)
|
||||
|
||||
var client openfeature.IClient = nil
|
||||
|
||||
|
@ -29,11 +31,11 @@ var {{ .Key | ToPascal }} = struct {
|
|||
// the evaluation error, if any, and the evaluation details.
|
||||
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
|
||||
}{
|
||||
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{ .Type | TypeString }}, error) {
|
||||
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
|
||||
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{- if eq (.Type | OpenFeatureType) "Object"}}any{{- else}}{{ .Type | TypeString }}{{- end}}, error) {
|
||||
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
|
||||
},
|
||||
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{ .Type | OpenFeatureType }}EvaluationDetails, error){
|
||||
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
|
||||
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{- if eq (.Type | OpenFeatureType) "Object"}}Interface{{- else }}{{ .Type | OpenFeatureType }}{{- end}}EvaluationDetails, error){
|
||||
return client.{{ .Type | OpenFeatureType }}ValueDetails(ctx, {{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "Object" }}{{.DefaultValue | ToMapLiteral }}{{- else }}{{ .DefaultValue | QuoteString }}{{- end}}, evalCtx)
|
||||
},
|
||||
}
|
||||
{{- end}}
|
||||
|
|
|
@ -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
|
||||
type GeneratorInfo struct {
|
||||
Name string
|
||||
Description string
|
||||
Stability Stability
|
||||
Creator GeneratorCreator
|
||||
Name string
|
||||
Description string
|
||||
Stability Stability
|
||||
Creator GeneratorCreator
|
||||
}
|
||||
|
||||
// GeneratorManager maintains a registry of available generators
|
||||
|
@ -34,10 +34,10 @@ func NewGeneratorManager() *GeneratorManager {
|
|||
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
|
||||
cmd := cmdCreator()
|
||||
m.generators[cmd.Use] = GeneratorInfo{
|
||||
Name: cmd.Use,
|
||||
Description: cmd.Short,
|
||||
Stability: Stability(cmd.Annotations["stability"]),
|
||||
Creator: cmdCreator,
|
||||
Name: cmd.Use,
|
||||
Description: cmd.Short,
|
||||
Stability: Stability(cmd.Annotations["stability"]),
|
||||
Creator: cmdCreator,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,11 +49,11 @@ func (m *GeneratorManager) GetAll() map[string]GeneratorInfo {
|
|||
// GetCommands returns cobra commands for all registered generators
|
||||
func (m *GeneratorManager) GetCommands() []*cobra.Command {
|
||||
var commands []*cobra.Command
|
||||
|
||||
|
||||
for _, info := range m.generators {
|
||||
commands = append(commands, info.Creator())
|
||||
}
|
||||
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
|
@ -62,14 +62,14 @@ func (m *GeneratorManager) PrintGeneratorsTable() error {
|
|||
tableData := [][]string{
|
||||
{"Generator", "Description", "Stability"},
|
||||
}
|
||||
|
||||
|
||||
// Get generator names for consistent ordering
|
||||
var names []string
|
||||
for name := range m.generators {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
|
||||
for _, name := range names {
|
||||
info := m.generators[name]
|
||||
tableData = append(tableData, []string{
|
||||
|
@ -78,7 +78,7 @@ func (m *GeneratorManager) PrintGeneratorsTable() error {
|
|||
string(info.Stability),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/flagset"
|
||||
|
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
|
|||
return "boolean"
|
||||
case flagset.StringType:
|
||||
return "string"
|
||||
case flagset.ObjectType:
|
||||
return "object"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toJSONString(value any) string {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
|
||||
funcs := template.FuncMap{
|
||||
"OpenFeatureType": openFeatureType,
|
||||
"ToJSONString": toJSONString,
|
||||
}
|
||||
|
||||
newParams := &generators.Params[any]{
|
||||
|
@ -49,8 +61,6 @@ func (g *NodejsGenerator) Generate(params *generators.Params[Params]) error {
|
|||
// NewGenerator creates a generator for NodeJS.
|
||||
func NewGenerator(fs *flagset.Flagset) *NodejsGenerator {
|
||||
return &NodejsGenerator{
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
||||
flagset.ObjectType: true,
|
||||
}),
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
OpenFeature,
|
||||
stringOrUndefined,
|
||||
objectOrUndefined,
|
||||
JsonValue,
|
||||
} from "@openfeature/server-sdk";
|
||||
import type {
|
||||
EvaluationContext,
|
||||
|
@ -17,30 +18,30 @@ export interface GeneratedClient {
|
|||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Key }}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{ .Type | OpenFeatureType }}`
|
||||
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||
*
|
||||
* Performs a flag evaluation that returns a {{ .Type | OpenFeatureType }}.
|
||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||
* @returns {Promise<{{ .Type | OpenFeatureType }}>} Flag evaluation response
|
||||
* @returns {Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>} Flag evaluation response
|
||||
*/
|
||||
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}>;
|
||||
{{ .Key | ToCamel }}(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>;
|
||||
|
||||
/**
|
||||
* {{ .Description }}
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Key }}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{ .Type | OpenFeatureType }}`
|
||||
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||
*
|
||||
* Performs a flag evaluation that a returns an evaluation details object.
|
||||
* @param {EvaluationContext} context The evaluation context used on an individual flag evaluation
|
||||
* @param {FlagEvaluationOptions} options Additional flag evaluation options
|
||||
* @returns {Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>} Flag evaluation details response
|
||||
* @returns {Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>} Flag evaluation details response
|
||||
*/
|
||||
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>>;
|
||||
{{ .Key | ToCamel }}Details(context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>>;
|
||||
{{ end -}}
|
||||
}
|
||||
|
||||
|
@ -74,12 +75,12 @@ export function getGeneratedClient(domainOrContext?: string | EvaluationContext,
|
|||
|
||||
return {
|
||||
{{- range .Flagset.Flags }}
|
||||
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ .Type | OpenFeatureType }}> => {
|
||||
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options);
|
||||
{{ .Key | ToCamel }}: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}> => {
|
||||
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
|
||||
},
|
||||
|
||||
{{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ .Type | OpenFeatureType }}>> => {
|
||||
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, context, options);
|
||||
{{ .Key | ToCamel }}Details: (context?: EvaluationContext, options?: FlagEvaluationOptions): Promise<EvaluationDetails<{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}>> => {
|
||||
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, context, options);
|
||||
},
|
||||
{{ end -}}
|
||||
{{ printf " " }}}
|
||||
|
|
|
@ -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 (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/flagset"
|
||||
|
@ -28,14 +29,25 @@ func openFeatureType(t flagset.FlagType) string {
|
|||
return "boolean"
|
||||
case flagset.StringType:
|
||||
return "string"
|
||||
case flagset.ObjectType:
|
||||
return "object"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toJSONString(value any) string {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
|
||||
funcs := template.FuncMap{
|
||||
"OpenFeatureType": openFeatureType,
|
||||
"ToJSONString": toJSONString,
|
||||
}
|
||||
|
||||
newParams := &generators.Params[any]{
|
||||
|
@ -49,8 +61,6 @@ func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
|
|||
// NewGenerator creates a generator for React.
|
||||
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
|
||||
return &ReactGenerator{
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
|
||||
flagset.ObjectType: true,
|
||||
}),
|
||||
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
type ReactFlagEvaluationNoSuspenseOptions,
|
||||
useFlag,
|
||||
useSuspenseFlag,
|
||||
JsonValue
|
||||
} from "@openfeature/react-sdk";
|
||||
{{ range .Flagset.Flags }}
|
||||
/**
|
||||
|
@ -12,11 +13,11 @@ import {
|
|||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Key }}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{ .Type | OpenFeatureType }}`
|
||||
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||
*/
|
||||
export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => {
|
||||
return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
|
||||
return useFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -24,13 +25,13 @@ export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) =
|
|||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Key }}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{ .Type | OpenFeatureType }}`
|
||||
* - default value: `{{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}`
|
||||
* - type: `{{ if eq (.Type | OpenFeatureType) "object" }}JsonValue{{ else }}{{ .Type | OpenFeatureType }}{{ end }}`
|
||||
*
|
||||
* Equivalent to useFlag with options: `{ suspend: true }`
|
||||
* @experimental — Suspense is an experimental feature subject to change in future versions.
|
||||
*/
|
||||
export const useSuspense{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
|
||||
return useSuspenseFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
|
||||
return useSuspenseFlag({{ .Key | Quote }}, {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, options);
|
||||
};
|
||||
{{ end}}
|
|
@ -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.
|
||||
func ToJSONSchema() *jsonschema.Schema {
|
||||
reflector := &jsonschema.Reflector{
|
||||
ExpandedStruct: true,
|
||||
ExpandedStruct: true,
|
||||
AllowAdditionalProperties: true,
|
||||
BaseSchemaID: "openfeature-cli",
|
||||
BaseSchemaID: "openfeature-cli",
|
||||
}
|
||||
|
||||
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
|
||||
|
@ -121,4 +121,4 @@ func ToJSONSchema() *jsonschema.Schema {
|
|||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/open-feature/cli/internal/filesystem"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type initManifest struct {
|
||||
|
@ -14,7 +15,7 @@ type initManifest struct {
|
|||
// Create creates a new manifest file at the given path.
|
||||
func Create(path string) error {
|
||||
m := &initManifest{
|
||||
Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
|
||||
Schema: "https://raw.githubusercontent.com/open-feature/cli/main/schema/v0/flag-manifest.json",
|
||||
Manifest: Manifest{
|
||||
Flags: map[string]any{},
|
||||
},
|
||||
|
@ -25,3 +26,19 @@ func Create(path string) error {
|
|||
}
|
||||
return filesystem.WriteFile(path, formattedInitManifest)
|
||||
}
|
||||
|
||||
// Load loads a manifest from a JSON file, unmarshals it, and returns a Manifest object.
|
||||
func Load(path string) (*Manifest, error) {
|
||||
fs := filesystem.FileSystem()
|
||||
data, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var m Manifest
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
|
|
@ -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,8 +29,8 @@ func main() {
|
|||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(data); err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err));
|
||||
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err))
|
||||
}
|
||||
|
||||
fmt.Println("JSON schema generated successfully at " + schemaPath)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,11 +49,11 @@ func walkPath(shouldPass bool, root string) error {
|
|||
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
|
||||
manifestLoader := gojsonschema.NewGoLoader(v)
|
||||
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error validating json schema: %v", err)
|
||||
}
|
||||
|
||||
if (len(result.Errors()) >= 1 && shouldPass == true) {
|
||||
if len(result.Errors()) >= 1 && shouldPass == true {
|
||||
var errorMessage strings.Builder
|
||||
|
||||
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
|
||||
|
@ -63,7 +63,7 @@ func walkPath(shouldPass bool, root string) error {
|
|||
return fmt.Errorf("%s", errorMessage.String())
|
||||
}
|
||||
|
||||
if (len(result.Errors()) == 0 && shouldPass == false) {
|
||||
if len(result.Errors()) == 0 && shouldPass == false {
|
||||
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
|
||||
}
|
||||
|
||||
|
|
|
@ -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