Compare commits

...

21 Commits
v0.3.5 ... main

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

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

* chore: remove packageRules from renovate.json

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

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

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

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

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

* fix: fix whitespace issue in Makefile

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

* fix: fix whitespace issue in Makefile

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

* fix: fix whitespaces in Makefile

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

---------

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

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

* fixed go integration test

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

* Update test/go-integration/test.go

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

* Update test/go-integration/test.go

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

* Update test/go-integration/test.go

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

* cleanup the test

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

---------

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

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

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

* chore: remove packageRules from renovate.json

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

---------

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

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

* chore: upgrade dependencies (#123)

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

---------

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

Signed-off-by: Adityasinghvats <131326798+Adityasinghvats@users.noreply.github.com>
2025-05-30 16:24:09 +00:00
64 changed files with 2067 additions and 209 deletions

View File

@ -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

View File

@ -57,7 +57,7 @@ jobs:
go-version-file: 'go.mod'
- name: Run all integration tests with Dagger
uses: dagger/dagger-for-github@v5
uses: dagger/dagger-for-github@b81317a976cb7f7125469707321849737cd1b3bc # v7
with:
workdir: .
verb: run

7
.gitignore vendored
View File

@ -28,4 +28,9 @@ dist
# openfeature cli config
.openfeature.yaml
.idea/
.idea/
node_modules/
npm-debug.log*
generated/
*.log

View File

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

View File

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

View File

@ -10,6 +10,16 @@ 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..."

View File

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

View File

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

View File

@ -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"
)

75
go.mod
View File

@ -2,60 +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
dagger.io/dagger v0.10.2 // indirect
github.com/99designs/gqlgen v0.17.31 // indirect
github.com/Khan/genqlient v0.6.0 // indirect
github.com/adrg/xdg v0.4.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/mitchellh/go-homedir v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // 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.6 // 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/sync v0.12.0 // 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
)

174
go.sum
View File

@ -6,12 +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.10.2 h1:8q4AwKm48qAWZdY7O2NMZ3FveteDHJeM3WwmcNKZOM0=
dagger.io/dagger v0.10.2/go.mod h1:AonAYX6ZXNGsVvro4HhB/Uzsp9FU+aI41YfAEY9f5mI=
github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158=
github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4=
github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk=
github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM=
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=
@ -21,44 +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.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
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/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
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/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
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=
@ -73,15 +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/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.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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=
@ -92,33 +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=
@ -128,12 +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.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
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=
@ -142,61 +164,107 @@ 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -199,10 +199,10 @@ func renderFlatDiff(changes []manifest.Change, cmd *cobra.Command) error {
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"`
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
@ -235,10 +235,10 @@ func renderJSONDiff(changes []manifest.Change, cmd *cobra.Command) error {
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"`
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

View File

@ -153,6 +153,38 @@ namespace TestNamespace
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>

View File

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

View File

@ -71,7 +71,27 @@ public final class OpenFeature {
* 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:
@ -131,7 +151,17 @@ public final class OpenFeature {
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);

View File

@ -8,8 +8,9 @@ import type {
EvaluationContext,
EvaluationDetails,
OpenFeatureModuleOptions,
JsonValue
} from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag } from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk";
import type { GeneratedClient } from "./openfeature";
import { getGeneratedClient } from "./openfeature";
@ -133,7 +134,7 @@ export function DiscountPercentage(props?: TypedFeatureProps): ParameterDecorato
* @Get("/")
* public async handleRequest(
* @EnableFeatureA()
* enableFeatureA: Observable<EvaluationDetails<number>>,
* enableFeatureA: Observable<EvaluationDetails<boolean>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
@ -158,7 +159,7 @@ export function EnableFeatureA(props?: TypedFeatureProps): ParameterDecorator {
* @Get("/")
* public async handleRequest(
* @GreetingMessage()
* greetingMessage: Observable<EvaluationDetails<number>>,
* greetingMessage: Observable<EvaluationDetails<string>>,
* )
* ```
* @param {TypedFeatureProps} props The options for injecting the feature flag.
@ -168,6 +169,31 @@ 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}.

View File

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

View File

@ -278,6 +278,94 @@ class GeneratedClient:
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,

View File

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

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"sort"
"strings"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/manifest"
@ -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
@ -132,3 +133,43 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
return nil
}
func FormatValidationError(issues []manifest.ValidationError) string {
var sb strings.Builder
sb.WriteString("flag manifest validation failed:\n\n")
// Group messages by flag path
grouped := make(map[string]struct {
flagType string
messages []string
})
for _, issue := range issues {
entry := grouped[issue.Path]
entry.flagType = issue.Type
entry.messages = append(entry.messages, issue.Message)
grouped[issue.Path] = entry
}
// Sort paths for consistent output
paths := make([]string, 0, len(grouped))
for path := range grouped {
paths = append(paths, path)
}
sort.Strings(paths)
// Format each row
for _, path := range paths {
entry := grouped[path]
flagType := entry.flagType
if flagType == "" {
flagType = "missing"
}
sb.WriteString(fmt.Sprintf(
"- flagType: %s\n flagPath: %s\n errors:\n ~ %s\n \tSuggestions:\n \t- flagType: boolean\n \t- defaultValue: true\n\n",
flagType,
path,
strings.Join(entry.messages, "\n ~ "),
))
}
return sb.String()
}

View File

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

View File

@ -3,6 +3,9 @@ package csharp
import (
_ "embed"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -31,6 +34,8 @@ func openFeatureType(t flagset.FlagType) string {
return "bool"
case flagset.StringType:
return "string"
case flagset.ObjectType:
return "object"
default:
return ""
}
@ -50,10 +55,77 @@ func formatDefaultValue(flag flagset.Flag) string {
}
}
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]{
@ -67,8 +139,6 @@ func (g *CsharpGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for C#.
func NewGenerator(fs *flagset.Flagset) *CsharpGenerator {
return &CsharpGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -64,13 +64,13 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
/// </summary>
/// <remarks>
/// <para>Flag key: {{ .Key }}</para>
/// <para>Default value: {{ .DefaultValue }}</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<{{ .Type | OpenFeatureType }}> {{ .Key | ToPascal }}Async(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
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);
@ -80,6 +80,8 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
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 }}
@ -90,13 +92,13 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
/// </summary>
/// <remarks>
/// <para>Flag key: {{ .Key }}</para>
/// <para>Default value: {{ .DefaultValue }}</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<{{ .Type | OpenFeatureType }}>> {{ .Key | ToPascal }}DetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
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);
@ -106,6 +108,8 @@ namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else
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 }}

View File

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

View File

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

View File

@ -2,7 +2,11 @@ package java
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -31,6 +35,8 @@ func openFeatureType(t flagset.FlagType) string {
return "Boolean"
case flagset.StringType:
return "String"
case flagset.ObjectType:
return "Object"
default:
return ""
}
@ -50,10 +56,84 @@ func formatDefaultValueForJava(flag flagset.Flag) string {
}
}
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]{
@ -67,8 +147,6 @@ func (g *JavaGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for Java.
func NewGenerator(fs *flagset.Flagset) *JavaGenerator {
return &JavaGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -17,7 +17,7 @@ public final class OpenFeature {
* Details:
* - Flag key: {{ .Key }}
* - Type: {{ .Type | OpenFeatureType }}
* - Default value: {{ .DefaultValue }}
* - Default value: {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ .DefaultValue }}{{ end }}
* Returns the flag value
*/
{{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx);
@ -27,7 +27,7 @@ public final class OpenFeature {
* Details:
* - Flag key: {{ .Key }}
* - Type: {{ .Type | OpenFeatureType }}
* - Default value: {{ .DefaultValue }}
* - 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);
@ -44,12 +44,12 @@ public final class OpenFeature {
{{ range .Flagset.Flags }}
@Override
public {{ .Type | OpenFeatureType }} {{ .Key | ToCamel }}(EvaluationContext ctx) {
return client.get{{ .Type | OpenFeatureType | ToPascal }}Value("{{ .Key }}", {{ . | FormatDefaultValue }}, 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 }}", {{ . | FormatDefaultValue }}, ctx);
return client.get{{ .Type | OpenFeatureType | ToPascal }}Details("{{ .Key }}", {{ if eq (.Type | OpenFeatureType) "Object" }}{{ .DefaultValue | ToMapLiteral }}{{ else }}{{ . | FormatDefaultValue }}{{ end }}, ctx);
}
{{ end }}
}

View File

@ -2,6 +2,7 @@ package nestjs
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 *NestJsGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
"ToJSONString": toJSONString,
}
newParams := &generators.Params[any]{
@ -49,8 +61,6 @@ func (g *NestJsGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for NestJS.
func NewGenerator(fs *flagset.Flagset) *NestJsGenerator {
return &NestJsGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -8,8 +8,9 @@ import type {
EvaluationContext,
EvaluationDetails,
OpenFeatureModuleOptions,
JsonValue
} from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag } from "@openfeature/nestjs-sdk";
import { OpenFeatureModule, BooleanFeatureFlag, StringFeatureFlag, NumberFeatureFlag, ObjectFeatureFlag } from "@openfeature/nestjs-sdk";
import type { GeneratedClient } from "./openfeature";
import { getGeneratedClient } from "./openfeature";
@ -100,21 +101,21 @@ interface TypedFeatureProps {
* **Details:**
* - flag key: `{{ .Key }}`
* - description: `{{ .Description }}`
* - 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 }}`
*
* Usage:
* ```typescript
* @Get("/")
* public async handleRequest(
* @{{ .Key | ToPascal }}()
* {{ .Key | ToCamel }}: Observable<EvaluationDetails<number>>,
* {{ .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: {{ .DefaultValue | QuoteString }}, ...props });
return {{ .Type | OpenFeatureType | ToPascal }}FeatureFlag({ flagKey: {{ .Key | Quote }}, defaultValue: {{ if eq (.Type | OpenFeatureType) "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}, ...props });
}
{{ end -}}

View File

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

View File

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

View File

@ -2,6 +2,11 @@ package python
import (
_ "embed"
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
"text/template"
"github.com/open-feature/cli/internal/flagset"
@ -43,6 +48,8 @@ func methodType(flagType flagset.FlagType) string {
return "boolean"
case flagset.FloatType:
return "float"
case flagset.ObjectType:
return "object"
default:
panic("unsupported flag type")
}
@ -64,7 +71,7 @@ func typedDetailsMethodAsync(flagType flagset.FlagType) string {
return "get_" + methodType(flagType) + "_details_async"
}
func pythonBoolLiteral(value interface{}) interface{} {
func pythonBoolLiteral(value any) any {
if v, ok := value.(bool); ok {
if v {
return "True"
@ -74,6 +81,63 @@ func pythonBoolLiteral(value interface{}) interface{} {
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,
@ -82,6 +146,7 @@ func (g *PythonGenerator) Generate(params *generators.Params[Params]) error {
"TypedDetailsMethodSync": typedDetailsMethodSync,
"TypedDetailsMethodAsync": typedDetailsMethodAsync,
"PythonBoolLiteral": pythonBoolLiteral,
"ToPythonDict": toPythonDict,
}
newParams := &generators.Params[any]{
@ -95,8 +160,6 @@ func (g *PythonGenerator) Generate(params *generators.Params[Params]) error {
// NewGenerator creates a generator for Python.
func NewGenerator(fs *flagset.Flagset) *PythonGenerator {
return &PythonGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{}),
}
}

View File

@ -25,14 +25,14 @@ class GeneratedClient:
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{ .DefaultValue | PythonBoolLiteral }}`
- 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={{ .DefaultValue | QuoteString | PythonBoolLiteral }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
@ -47,14 +47,14 @@ class GeneratedClient:
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{ .DefaultValue | PythonBoolLiteral }}`
- 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={{ .DefaultValue | QuoteString | PythonBoolLiteral }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
@ -69,14 +69,14 @@ class GeneratedClient:
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{ .DefaultValue | PythonBoolLiteral }}`
- 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={{ .DefaultValue | QuoteString | PythonBoolLiteral }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)
@ -91,14 +91,14 @@ class GeneratedClient:
**Details:**
- flag key: `{{ .Key }}`
- default value: `{{ .DefaultValue | PythonBoolLiteral }}`
- 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={{ .DefaultValue | QuoteString | PythonBoolLiteral }},
default_value={{- if eq (.Type | OpenFeatureType) "object"}}{{.DefaultValue | ToPythonDict }}{{- else }}{{ .DefaultValue | QuoteString | PythonBoolLiteral }}{{- end }},
evaluation_context=evaluation_context,
flag_evaluation_options=flag_evaluation_options,
)

View File

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

View File

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

View File

@ -32,4 +32,4 @@ func GetValidOutputFormats() []string {
string(OutputFormatJSON),
string(OutputFormatYAML),
}
}
}

4
renovate.json Normal file
View File

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

View File

@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="OpenFeature" Version="2.3.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="OpenFeature" Version="2.7.0" />
</ItemGroup>
<ItemGroup>

View File

@ -111,6 +111,38 @@ namespace OpenFeature
{
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.

View File

@ -159,15 +159,15 @@ namespace TestNamespace
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: map[primaryColor:#007bff secondaryColor:#6c757d]</para>
/// <para>Type: Value</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)
{
throw new NotSupportedException("Unsupported flag type");
return await _client.GetObjectValueAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>
@ -175,15 +175,15 @@ namespace TestNamespace
/// </summary>
/// <remarks>
/// <para>Flag key: themeCustomization</para>
/// <para>Default value: map[primaryColor:#007bff secondaryColor:#6c757d]</para>
/// <para>Type: Value</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)
{
throw new NotSupportedException("Unsupported flag type");
return await _client.GetObjectDetailsAsync("themeCustomization", new Value(Structure.Builder().Set("primaryColor", "#007bff").Set("secondaryColor", "#6c757d").Build()), evaluationContext, options);
}
/// <summary>

View File

@ -152,6 +152,38 @@ namespace TestNamespace
{
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.

View File

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

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

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

View File

@ -39,7 +39,7 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe
From("golang:1.24-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "cli"})
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
// Generate C# client
generated := cli.WithExec([]string{

View File

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

View File

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

View File

@ -20,6 +20,23 @@ func main() {
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 ===")

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

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

View File

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

View File

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