refactor!: add init command, update cli flags, support a config file (#71)

Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
This commit is contained in:
Michael Beemer 2025-03-14 16:12:26 -04:00 committed by GitHub
parent e430a8dbe6
commit 106bf9ddfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 2181 additions and 1048 deletions

View File

@ -27,5 +27,5 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
version: v1.64
only-new-issues: true

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ go.work.sum
# env file
.env
dist
# openfeature cli config
.openfeature.yaml

96
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,96 @@
TODO: Add contributing guidelines
## Templates
### Data
The `TemplateData` struct is used to pass data to the templates.
### Built-in template functions
The following functions are automatically included in the templates:
#### ToPascal
Converts a string to `PascalCase`
```go
{{ "hello world" | ToPascal }} // HelloWorld
```
#### ToCamel
Converts a string to `camelCase`
```go
{{ "hello world" | ToCamel }} // helloWorld
```
#### ToKebab
Converts a string to `kebab-case`
```go
{{ "hello world" | ToKebab }} // hello-world
```
#### ToSnake
Converts a string to `snake_case`
```go
{{ "hello world" | ToSnake }} // hello_world
```
#### ToScreamingSnake
Converts a string to `SCREAMING_SNAKE_CASE`
```go
{{ "hello world" | ToScreamingSnake }} // HELLO_WORLD
```
#### ToUpper
Converts a string to `UPPER CASE`
```go
{{ "hello world" | ToUpper }} // HELLO WORLD
```
#### ToLower
Converts a string to `lower case`
```go
{{ "HELLO WORLD" | ToLower }} // hello world
```
#### ToTitle
Converts a string to `Title Case`
```go
{{ "hello world" | ToTitle }} // Hello World
```
#### Quote
Wraps a string in double quotes
```go
{{ "hello world" | Quote }} // "hello world"
```
#### QuoteString
Wraps only strings in double quotes
```go
{{ "hello world" | QuoteString }} // "hello world"
{{ 123 | QuoteString }} // 123
```
### Custom template functions
You can add custom template functions by passing a `FuncMap` to the `GenerateFile` function.

View File

@ -2,10 +2,15 @@
.PHONY: test
test:
@echo "Running tests..."
go test -v ./...
@go test -v ./...
@echo "Tests passed successfully!"
generate-docs:
@echo "Generating documentation..."
go run ./docs/generate-commands.go
@echo "Documentation generated successfully!"
@go run ./docs/generate-commands.go
@echo "Documentation generated successfully!"
generate-schema:
@echo "Generating schema..."
@go run ./schema/generate-schema.go
@echo "Schema generated successfully!"

71
cmd/config.go Normal file
View File

@ -0,0 +1,71 @@
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// initializeConfig reads in config file and ENV variables if set.
// It applies configuration values to command flags based on hierarchical priority.
func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
v := viper.New()
// Set the config file name and path
v.SetConfigName(".openfeature")
v.AddConfigPath(".")
// Read the config file
if err := v.ReadInConfig(); err != nil {
// It's okay if there isn't a config file
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
}
}
// Track which flags were set directly via command line
cmdLineFlags := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
cmdLineFlags[f.Name] = true
})
// Apply the configuration values
cmd.Flags().VisitAll(func(f *pflag.Flag) {
// Skip if flag was set on command line
if cmdLineFlags[f.Name] {
return
}
// Build configuration paths from most specific to least specific
configPaths := []string{}
// Check the most specific path (e.g., generate.go.package-name)
if bindPrefix != "" {
configPaths = append(configPaths, bindPrefix + "." + f.Name)
// Check parent paths (e.g., generate.package-name)
parts := strings.Split(bindPrefix, ".")
for i := len(parts) - 1; i > 0; i-- {
parentPath := strings.Join(parts[:i], ".") + "." + f.Name
configPaths = append(configPaths, parentPath)
}
}
// Check the base path (e.g., package-name)
configPaths = append(configPaths, f.Name)
// Try each path in order until we find a match
for _, path := range configPaths {
if v.IsSet(path) {
val := v.Get(path)
_ = f.Value.Set(fmt.Sprintf("%v", val))
break
}
}
})
return nil
}

172
cmd/config_test.go Normal file
View File

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

View File

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

148
cmd/generate.go Normal file
View File

@ -0,0 +1,148 @@
package cmd
import (
"strings"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
"github.com/open-feature/cli/internal/generators/golang"
"github.com/open-feature/cli/internal/generators/react"
"github.com/spf13/cobra"
)
// addStabilityInfo adds stability information to the command's help template before "Usage:"
func addStabilityInfo(cmd *cobra.Command) {
// Only modify commands that have a stability annotation
if stability, ok := cmd.Annotations["stability"]; ok {
originalTemplate := cmd.UsageTemplate()
// Find the "Usage:" section and insert stability info before it
if strings.Contains(originalTemplate, "Usage:") {
customTemplate := strings.Replace(
originalTemplate,
"Usage:",
"Stability: " + stability + "\n\nUsage:",
1, // Replace only the first occurrence
)
cmd.SetUsageTemplate(customTemplate)
} else {
// Fallback if "Usage:" not found - prepend to the template
customTemplate := "Stability: " + stability + "\n\n" + originalTemplate
cmd.SetUsageTemplate(customTemplate)
}
}
}
func GetGenerateReactCmd() *cobra.Command {
reactCmd := &cobra.Command{
Use: "react",
Short: "Generate typesafe React Hooks.",
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.react")
},
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
params := generators.Params[react.Params]{
OutputPath: outputPath,
Custom: react.Params{},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := react.NewGenerator(flagset)
err = generator.Generate(&params)
if err != nil {
return err
}
return nil
},
}
addStabilityInfo(reactCmd)
return reactCmd
}
func GetGenerateGoCmd() *cobra.Command {
goCmd := &cobra.Command{
Use: "go",
Short: "Generate typesafe accessors for OpenFeature.",
Long: `Generate typesafe accessors compatible with the OpenFeature Go SDK.`,
Annotations: map[string]string{
"stability": string(generators.Alpha),
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate.go")
},
RunE: func(cmd *cobra.Command, args []string) error {
goPackageName := config.GetGoPackageName(cmd)
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
params := generators.Params[golang.Params]{
OutputPath: outputPath,
Custom: golang.Params{
GoPackage: goPackageName,
},
}
flagset, err := flagset.Load(manifestPath)
if err != nil {
return err
}
generator := golang.NewGenerator(flagset)
err = generator.Generate(&params)
if err != nil {
return err
}
return nil
},
}
// Add Go-specific flags
config.AddGoGenerateFlags(goCmd)
addStabilityInfo(goCmd)
return goCmd
}
func init() {
// Register generators with the manager
generators.DefaultManager.Register(GetGenerateReactCmd)
generators.DefaultManager.Register(GetGenerateGoCmd)
}
func GetGenerateCmd() *cobra.Command {
generateCmd := &cobra.Command{
Use: "generate",
Short: "Generate typesafe OpenFeature accessors.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "generate")
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Println("Available generators:")
return generators.DefaultManager.PrintGeneratorsTable()
},
}
// Add generate flags using the config package
config.AddGenerateFlags(generateCmd)
// Add all registered generator commands
for _, subCmd := range generators.DefaultManager.GetCommands() {
generateCmd.AddCommand(subCmd)
}
return generateCmd
}

View File

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

View File

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

View File

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

View File

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

128
cmd/generate_test.go Normal file
View File

@ -0,0 +1,128 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/filesystem"
"github.com/spf13/afero"
)
// generateTestCase holds the configuration for each generate test
type generateTestCase struct {
name string // test case name
command string // generator to run
manifestGolden string // path to the golden manifest file
outputGolden string // path to the golden output file
outputPath string // output directory (optional, defaults to "output")
outputFile string // output file name
packageName string // optional, only used for Go
}
func TestGenerate(t *testing.T) {
testCases := []generateTestCase{
{
name: "Go generation success",
command: "go",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_go.golden",
outputFile: "testpackage.go",
packageName: "testpackage",
},
{
name: "React generation success",
command: "react",
manifestGolden: "testdata/success_manifest.golden",
outputGolden: "testdata/success_react.golden",
outputFile: "openfeature.ts",
},
// Add more test cases here as needed
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := GetGenerateCmd()
// global flag exists on root only.
config.AddRootFlags(cmd)
// Constant paths
const memoryManifestPath = "manifest/path.json"
// Use default output path if not specified
outputPath := tc.outputPath
if outputPath == "" {
outputPath = "output"
}
// Prepare in-memory files
fs := afero.NewMemMapFs()
filesystem.SetFileSystem(fs)
readOsFileAndWriteToMemMap(t, tc.manifestGolden, memoryManifestPath, fs)
// Prepare command arguments
args := []string{
tc.command,
"--manifest", memoryManifestPath,
"--output", outputPath,
}
// Add package name if provided (for Go)
if tc.packageName != "" {
args = append(args, "--package-name", tc.packageName)
}
cmd.SetArgs(args)
// Run command
err := cmd.Execute()
if err != nil {
t.Error(err)
}
// Compare result
compareOutput(t, tc.outputGolden, filepath.Join(outputPath, tc.outputFile), fs)
})
}
}
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
data, err := os.ReadFile(inputPath)
if (err != nil) {
t.Fatalf("error reading file %q: %v", inputPath, err)
}
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err)
}
f, err := memFs.Create(memPath)
if err != nil {
t.Fatalf("error creating file %q: %v", memPath, err)
}
defer f.Close()
writtenBytes, err := f.Write(data)
if err != nil {
t.Fatalf("error writing contents to file %q: %v", memPath, err)
}
if writtenBytes != len(data) {
t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath)
}
}
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
want, err := os.ReadFile(testFile)
if err != nil {
t.Fatalf("error reading file %q: %v", testFile, err)
}
got, err := afero.ReadFile(fs, memoryOutputPath)
if err != nil {
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("output mismatch (-want +got):\n%s", diff)
}
}

51
cmd/init.go Normal file
View File

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

View File

@ -1,10 +1,10 @@
package cmd
import (
"fmt"
"os"
"github.com/open-feature/cli/cmd/generate"
"github.com/open-feature/cli/internal/config"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
@ -15,27 +15,58 @@ var (
Date string
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "openfeature",
Short: "CLI for OpenFeature.",
Long: `CLI for OpenFeature related functionalities.`,
DisableAutoGenTag: true,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(version string, commit string, date string) {
Version = version
Commit = commit
Date = date
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
if err := GetRootCmd().Execute(); err != nil {
pterm.Error.Println(err)
os.Exit(1)
}
}
func init() {
rootCmd.AddCommand(generate.Root)
rootCmd.AddCommand(versionCmd)
func GetRootCmd() *cobra.Command {
// Execute all parent's persistent hooks
cobra.EnableTraverseRunHooks =true
rootCmd := &cobra.Command{
Use: "openfeature",
Short: "CLI for OpenFeature.",
Long: `CLI for OpenFeature related functionalities.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "")
},
RunE: func(cmd *cobra.Command, args []string) error {
printBanner()
pterm.Println()
pterm.Println("To see all the options, try 'openfeature --help'")
pterm.Println()
return nil
},
SilenceErrors: true,
SilenceUsage: true,
DisableSuggestions: false,
SuggestionsMinimumDistance: 2,
DisableAutoGenTag: true,
}
// Add global flags using the config package
config.AddRootFlags(rootCmd)
// Add subcommands
rootCmd.AddCommand(GetVersionCmd())
rootCmd.AddCommand(GetInitCmd())
rootCmd.AddCommand(GetGenerateCmd())
// Add a custom error handler after the command is created
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
pterm.Error.Printf("Invalid flag: %s", err)
pterm.Println("Run 'openfeature --help' for usage information")
return err
})
return rootCmd
}

View File

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

View File

@ -15,7 +15,7 @@ import {
* - default value: `0.15`
* - type: `number`
*/
export const useDiscountPercentage = (options: ReactFlagEvaluationOptions) => {
export const useDiscountPercentage = (options?: ReactFlagEvaluationOptions) => {
return useFlag("discountPercentage", 0.15, options);
};
@ -30,7 +30,7 @@ export const useDiscountPercentage = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseDiscountPercentage = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseDiscountPercentage = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("discountPercentage", 0.15, options);
};
@ -42,7 +42,7 @@ export const useSuspenseDiscountPercentage = (options: ReactFlagEvaluationNoSusp
* - default value: `false`
* - type: `boolean`
*/
export const useEnableFeatureA = (options: ReactFlagEvaluationOptions) => {
export const useEnableFeatureA = (options?: ReactFlagEvaluationOptions) => {
return useFlag("enableFeatureA", false, options);
};
@ -57,7 +57,7 @@ export const useEnableFeatureA = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseEnableFeatureA = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseEnableFeatureA = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("enableFeatureA", false, options);
};
@ -69,7 +69,7 @@ export const useSuspenseEnableFeatureA = (options: ReactFlagEvaluationNoSuspense
* - default value: `Hello there!`
* - type: `string`
*/
export const useGreetingMessage = (options: ReactFlagEvaluationOptions) => {
export const useGreetingMessage = (options?: ReactFlagEvaluationOptions) => {
return useFlag("greetingMessage", "Hello there!", options);
};
@ -84,7 +84,7 @@ export const useGreetingMessage = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseGreetingMessage = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseGreetingMessage = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("greetingMessage", "Hello there!", options);
};
@ -96,7 +96,7 @@ export const useSuspenseGreetingMessage = (options: ReactFlagEvaluationNoSuspens
* - default value: `50`
* - type: `number`
*/
export const useUsernameMaxLength = (options: ReactFlagEvaluationOptions) => {
export const useUsernameMaxLength = (options?: ReactFlagEvaluationOptions) => {
return useFlag("usernameMaxLength", 50, options);
};
@ -111,6 +111,6 @@ export const useUsernameMaxLength = (options: ReactFlagEvaluationOptions) => {
* Equivalent to useFlag with options: `{ suspend: true }`
* @experimental — Suspense is an experimental feature subject to change in future versions.
*/
export const useSuspenseUsernameMaxLength = (options: ReactFlagEvaluationNoSuspenseOptions) => {
export const useSuspenseUsernameMaxLength = (options?: ReactFlagEvaluationNoSuspenseOptions) => {
return useSuspenseFlag("usernameMaxLength", 50, options);
};

20
cmd/utils.go Normal file
View File

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

View File

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

View File

@ -8,14 +8,21 @@ CLI for OpenFeature.
CLI for OpenFeature related functionalities.
```
openfeature [flags]
```
### Options
```
-h, --help help for openfeature
-h, --help help for openfeature
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature.
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
* [openfeature init](openfeature_init.md) - Initialize a new project
* [openfeature version](openfeature_version.md) - Print the version number of the OpenFeature CLI

View File

@ -2,23 +2,29 @@
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate
Code generation for flag accessors for OpenFeature.
Generate typesafe OpenFeature accessors.
### Synopsis
Code generation for flag accessors for OpenFeature.
```
openfeature generate [flags]
```
### Options
```
--flag_manifest_path string Path to the flag manifest.
-h, --help help for generate
--output_path string Output path for the codegen
-h, --help help for generate
-o, --output string Path to where the generated files should be saved
```
### Options inherited from parent commands
```
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.
* [openfeature generate go](openfeature_generate_go.md) - Generate Golang flag accessors for OpenFeature.
* [openfeature generate go](openfeature_generate_go.md) - Generate typesafe accessors for OpenFeature.
* [openfeature generate react](openfeature_generate_react.md) - Generate typesafe React Hooks.

View File

@ -2,11 +2,11 @@
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature generate go
Generate Golang flag accessors for OpenFeature.
Generate typesafe accessors for OpenFeature.
### Synopsis
Generate Golang flag accessors for OpenFeature.
Generate typesafe accessors compatible with the OpenFeature Go SDK.
```
openfeature generate go [flags]
@ -16,17 +16,18 @@ openfeature generate go [flags]
```
-h, --help help for go
--package_name string Name of the Go package to be generated.
--package-name string Name of the generated Go package (default "openfeature")
```
### Options inherited from parent commands
```
--flag_manifest_path string Path to the flag manifest.
--output_path string Output path for the codegen
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature.
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -21,11 +21,12 @@ openfeature generate react [flags]
### Options inherited from parent commands
```
--flag_manifest_path string Path to the flag manifest.
--output_path string Output path for the codegen
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
-o, --output string Path to where the generated files should be saved
```
### SEE ALSO
* [openfeature generate](openfeature_generate.md) - Code generation for flag accessors for OpenFeature.
* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.

View File

@ -0,0 +1,32 @@
<!-- markdownlint-disable-file -->
<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->
## openfeature init
Initialize a new project
### Synopsis
Initialize a new project for OpenFeature CLI.
```
openfeature init [flags]
```
### Options
```
-h, --help help for init
--override Override an existing configuration
```
### Options inherited from parent commands
```
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.

View File

@ -14,6 +14,13 @@ openfeature version [flags]
-h, --help help for version
```
### Options inherited from parent commands
```
-m, --manifest string Path to the flag manifest (default "flags.json")
--no-input Disable interactive prompts
```
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.

View File

@ -1,16 +1,27 @@
package main
import (
"log"
"fmt"
"os"
"github.com/open-feature/cli/cmd"
"github.com/spf13/cobra/doc"
)
const docPath = "./docs/commands"
// GenerateDoc generates cobra docs of the cmd
// Generates cobra docs of the cmd
func main() {
if err := cmd.GenerateDoc(docPath); err != nil {
log.Fatal(err)
linkHandler := func(name string) string {
return name
}
filePrepender := func(filename string) string {
return "<!-- markdownlint-disable-file -->\n<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->\n"
}
if err := doc.GenMarkdownTreeCustom(cmd.GetRootCmd(), docPath, filePrepender, linkHandler); err != nil {
fmt.Fprintf(os.Stderr, "error generating docs: %v\n", err)
os.Exit(1)
}
}

33
go.mod
View File

@ -1,32 +1,55 @@
module github.com/open-feature/cli
go 1.22.5
go 1.23.0
toolchain go1.24.0
require (
github.com/iancoleman/strcase v0.3.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/invopop/jsonschema v0.13.0
github.com/pterm/pterm v0.12.80
github.com/spf13/afero v1.11.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.10.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/text v0.23.0
)
require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // 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/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

129
go.sum
View File

@ -1,3 +1,27 @@
atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=
atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=
atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=
atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
github.com/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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -10,18 +34,40 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/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=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@ -29,6 +75,18 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg=
github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo=
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -37,8 +95,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@ -56,28 +114,85 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/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/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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/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/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/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

@ -0,0 +1,72 @@
package config
import (
"github.com/spf13/cobra"
)
// Flag name constants to avoid duplication
const (
ManifestFlagName = "manifest"
OutputFlagName = "output"
NoInputFlagName = "no-input"
GoPackageFlagName = "package-name"
OverrideFlagName = "override"
)
// Default values for flags
const (
DefaultManifestPath = "flags.json"
DefaultOutputPath = ""
DefaultGoPackageName = "openfeature"
)
// AddRootFlags adds the common flags to the given command
func AddRootFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringP(ManifestFlagName, "m", DefaultManifestPath, "Path to the flag manifest")
cmd.PersistentFlags().Bool(NoInputFlagName, false, "Disable interactive prompts")
}
// AddGenerateFlags adds the common generate flags to the given command
func AddGenerateFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringP(OutputFlagName, "o", DefaultOutputPath, "Path to where the generated files should be saved")
}
// AddGoGenerateFlags adds the go generator specific flags to the given command
func AddGoGenerateFlags(cmd *cobra.Command) {
cmd.Flags().String(GoPackageFlagName, DefaultGoPackageName, "Name of the generated Go package")
}
// AddInitFlags adds the init command specific flags
func AddInitFlags(cmd *cobra.Command) {
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
}
// GetManifestPath gets the manifest path from the given command
func GetManifestPath(cmd *cobra.Command) string {
manifestPath, _ := cmd.Flags().GetString(ManifestFlagName)
return manifestPath
}
// GetOutputPath gets the output path from the given command
func GetOutputPath(cmd *cobra.Command) string {
outputPath, _ := cmd.Flags().GetString(OutputFlagName)
return outputPath
}
// GetGoPackageName gets the Go package name from the given command
func GetGoPackageName(cmd *cobra.Command) string {
goPackageName, _ := cmd.Flags().GetString(GoPackageFlagName)
return goPackageName
}
// GetNoInput gets the no-input flag from the given command
func GetNoInput(cmd *cobra.Command) bool {
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)
return noInput
}
// GetOverride gets the override flag from the given command
func GetOverride(cmd *cobra.Command) bool {
override, _ := cmd.Flags().GetBool(OverrideFlagName)
return override
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
package generators
import (
"strconv"
"strings"
"text/template"
"github.com/iancoleman/strcase"
"golang.org/x/text/cases"
)
func defaultFuncs() template.FuncMap {
// Update the contributing doc when adding a new function
return template.FuncMap{
// Remapping ToCamel to ToPascal to match the expected behavior
// Ref: https://github.com/iancoleman/strcase/issues/53
"ToPascal": strcase.ToCamel,
// Remapping ToLowerCamel to ToCamel to match the expected behavior
// Ref: See above
"ToCamel": strcase.ToLowerCamel,
"ToKebab": strcase.ToKebab,
"ToScreamingKebab": strcase.ToScreamingKebab,
"ToSnake": strcase.ToSnake,
"ToScreamingSnake": strcase.ToScreamingSnake,
"ToUpper": strings.ToUpper,
"ToLower": strings.ToLower,
"Title": cases.Title,
"Quote": strconv.Quote,
"QuoteString": func (input any) any {
if str, ok := input.(string); ok {
return strconv.Quote(str)
}
return input
},
}
}
func init() {
// results in "Api" using ToCamel("API")
// results in "api" using ToLowerCamel("API")
strcase.ConfigureAcronym("API", "api")
}

View File

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

View File

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

View File

@ -1,8 +1,8 @@
// AUTOMATICALLY GENERATED BY OPENFEATURE CODEGEN, DO NOT EDIT.
package {{.GoPackage}}
// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
package {{ .Params.Custom.GoPackage }}
import (
{{- range $_, $p := SupportImports .Flags}}
{{- range $_, $p := SupportImports .Flagset.Flags}}
{{$p}}
{{- end}}
)
@ -18,27 +18,26 @@ type StringProviderDetails func(ctx context.Context, evalCtx openfeature.Evaluat
var client openfeature.IClient = nil
{{- range .Flags}}
// {{.Docs}}
var {{FlagVarName .Name}} = struct {
// Value returns the value of the flag {{FlagVarName .Name}},
{{- range .Flagset.Flags }}
// {{.Description}}
var {{ .Key | ToPascal }} = struct {
// Value returns the value of the flag {{ .Key | ToPascal }},
// as well as the evaluation error, if present.
Value {{OpenFeatureType .Type}}Provider
Value {{ .Type | OpenFeatureType }}Provider
// ValueWithDetails returns the value of the flag {{FlagVarName .Name}},
// ValueWithDetails returns the value of the flag {{ .Key | ToPascal }},
// the evaluation error, if any, and the evaluation details.
ValueWithDetails {{OpenFeatureType .Type}}ProviderDetails
ValueWithDetails {{ .Type | OpenFeatureType }}ProviderDetails
}{
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{TypeString .Type}}, error) {
return client.{{OpenFeatureType .Type}}Value(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, evalCtx)
Value: func(ctx context.Context, evalCtx openfeature.EvaluationContext) ({{ .Type | TypeString }}, error) {
return client.{{ .Type | OpenFeatureType }}Value(ctx, {{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, evalCtx)
},
ValueWithDetails: func(ctx context.Context, evalCtx openfeature.EvaluationContext) (openfeature.{{OpenFeatureType .Type}}EvaluationDetails, error){
return client.{{OpenFeatureType .Type}}ValueDetails(ctx, {{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, 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)
},
}
{{- end}}
func init() {
client = openfeature.GetApiInstance().GetClient()
client = openfeature.GetApiInstance().GetClient()
}

View File

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

View File

@ -0,0 +1,56 @@
package react
import (
_ "embed"
"text/template"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
)
type ReactGenerator struct {
generators.CommonGenerator
}
type Params struct {
}
//go:embed react.tmpl
var reactTmpl string
func openFeatureType(t flagset.FlagType) string {
switch t {
case flagset.IntType:
fallthrough
case flagset.FloatType:
return "number"
case flagset.BoolType:
return "boolean"
case flagset.StringType:
return "string"
default:
return ""
}
}
func (g *ReactGenerator) Generate(params *generators.Params[Params]) error {
funcs := template.FuncMap{
"OpenFeatureType": openFeatureType,
}
newParams := &generators.Params[any]{
OutputPath: params.OutputPath,
Custom: Params{},
}
return g.GenerateFile(funcs, reactTmpl, newParams, "openfeature.ts")
}
// NewGenerator creates a generator for React.
func NewGenerator(fs *flagset.Flagset) *ReactGenerator {
return &ReactGenerator{
CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
flagset.ObjectType: true,
}),
}
}

View File

@ -0,0 +1,36 @@
'use client';
import {
type ReactFlagEvaluationOptions,
type ReactFlagEvaluationNoSuspenseOptions,
useFlag,
useSuspenseFlag,
} from "@openfeature/react-sdk";
{{ range .Flagset.Flags }}
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
*/
export const use{{ .Key | ToPascal }} = (options?: ReactFlagEvaluationOptions) => {
return useFlag({{ .Key | Quote }}, {{ .DefaultValue | QuoteString }}, options);
};
/**
* {{ .Description }}
*
* **Details:**
* - flag key: `{{ .Key }}`
* - default value: `{{ .DefaultValue }}`
* - type: `{{ .Type | OpenFeatureType }}`
*
* 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);
};
{{ end}}

View File

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

View File

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

View File

@ -0,0 +1,27 @@
package manifest
import (
"encoding/json"
"github.com/open-feature/cli/internal/filesystem"
)
type initManifest struct {
Schema string `json:"$schema,omitempty"`
Manifest
}
// Create creates a new manifest file at the given path.
func Create(path string) error {
m := &initManifest{
Schema: "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
Manifest: Manifest{
Flags: map[string]any{},
},
}
formattedInitManifest, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
return filesystem.WriteFile(path, formattedInitManifest)
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
// Package flagmanifest embeds the flag manifest into a code module.
package flagmanifest
import _ "embed"
// Schema contains the embedded flag manifest schema.
//
//go:embed flag_manifest.json
var Schema string
// SchemaPath provides the current path and version of flag manifest.
const SchemaPath = "github.com/open-feature/cli/schema/v0/flag_manifest.json"

View File

@ -1,72 +0,0 @@
package flagmanifest
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/santhosh-tekuri/jsonschema/v5"
)
var compiledFlagManifestSchema *jsonschema.Schema
func init() {
sch, err := jsonschema.CompileString(SchemaPath, Schema)
if err != nil {
log.Fatal(fmt.Errorf("error compiling JSON schema: %v", err))
}
compiledFlagManifestSchema = sch
}
func TestPositiveFlagManifest(t *testing.T) {
if err := walkPath(true, "./tests/positive"); err != nil {
t.Error(err)
t.FailNow()
}
}
func TestNegativeFlagManifest(t *testing.T) {
if err := walkPath(false, "./tests/negative"); err != nil {
t.Error(err)
t.FailNow()
}
}
func walkPath(shouldPass bool, root string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
ps := strings.Split(path, ".")
if ps[len(ps)-1] != "json" {
return nil
}
file, err := os.ReadFile(path)
if err != nil {
return err
}
var v interface{}
if err := json.Unmarshal([]byte(file), &v); err != nil {
log.Fatal(err)
}
err = compiledFlagManifestSchema.Validate(v)
if (err != nil && shouldPass == true) {
return fmt.Errorf("file %s should not have failed validation, but did: %s", path, err)
}
if (err == nil && shouldPass == false) {
return fmt.Errorf("file %s should have failed validation, but did not", path)
}
return nil
})
}

9
schema/v0/schema.go Normal file
View File

@ -0,0 +1,9 @@
// Package schema embeds the flag manifest into a code module.
package schema
import _ "embed"
// Schema contains the embedded flag manifest schema.
//
//go:embed flag-manifest.json
var SchemaFile string

72
schema/v0/schema_test.go Normal file
View File

@ -0,0 +1,72 @@
package schema
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/xeipuuv/gojsonschema"
)
func TestPositiveFlagManifest(t *testing.T) {
if err := walkPath(true, "./testdata/positive"); err != nil {
t.Error(err)
t.FailNow()
}
}
func TestNegativeFlagManifest(t *testing.T) {
if err := walkPath(false, "./testdata/negative"); err != nil {
t.Error(err)
t.FailNow()
}
}
func walkPath(shouldPass bool, root string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
ps := strings.Split(path, ".")
if ps[len(ps)-1] != "json" {
return nil
}
file, err := os.ReadFile(path)
if err != nil {
return err
}
var v any
if err := json.Unmarshal([]byte(file), &v); err != nil {
log.Fatal(err)
}
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
manifestLoader := gojsonschema.NewGoLoader(v)
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
if (err != nil) {
return fmt.Errorf("Error validating json schema: %v", err)
}
if (len(result.Errors()) >= 1 && shouldPass == true) {
var errorMessage strings.Builder
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
for _, error := range result.Errors() {
errorMessage.WriteString(" - " + error.String() + "\n")
}
return fmt.Errorf("%s", errorMessage.String())
}
if (len(result.Errors()) == 0 && shouldPass == false) {
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
}
return nil
})
}

View File

@ -0,0 +1,10 @@
{
"$schema": "../../flag-manifest.json",
"flags": {
"": {
"flagType": "boolean",
"defaultValue": true,
"description": "A flag that tests the invalid character case in flag keys."
}
}
}

View File

@ -1,5 +1,5 @@
{
"$schema": "../../flag_manifest.json",
"$schema": "../../flag-manifest.json",
"flags": {
"booleanFlag": {
"codeDefault": true

View File

@ -1,5 +1,5 @@
{
"$schema": "../../flag_manifest.json",
"$schema": "../../flag-manifest.json",
"flags": {
"booleanFlag": {
"flagType": "boolean",

View File

@ -1,4 +0,0 @@
{
"$schema": "../../flag_manifest.json",
"flags": {}
}