mirror of https://github.com/open-feature/cli.git
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:
parent
e430a8dbe6
commit
106bf9ddfe
|
|
@ -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
|
||||
|
|
@ -24,3 +24,6 @@ go.work.sum
|
|||
# env file
|
||||
.env
|
||||
dist
|
||||
|
||||
# openfeature cli config
|
||||
.openfeature.yaml
|
||||
|
|
@ -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.
|
||||
11
Makefile
11
Makefile
|
|
@ -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!"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
23
cmd/docs.go
23
cmd/docs.go
|
|
@ -1,23 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
// GenerateDoc generates cobra docs of the cmd
|
||||
func GenerateDoc(path string) error {
|
||||
linkHandler := func(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
filePrepender := func(filename string) string {
|
||||
return "<!-- markdownlint-disable-file -->\n<!-- WARNING: THIS DOC IS AUTO-GENERATED. DO NOT EDIT! -->\n"
|
||||
}
|
||||
|
||||
if err := doc.GenMarkdownTreeCustom(rootCmd, path, filePrepender, linkHandler); err != nil {
|
||||
return fmt.Errorf("error generating docs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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(¶ms)
|
||||
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(¶ms)
|
||||
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
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"github.com/open-feature/cli/cmd/generate/golang"
|
||||
"github.com/open-feature/cli/cmd/generate/react"
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Root for `generate“ sub-commands, handling code generation for flag accessors.
|
||||
var Root = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Code generation for flag accessors for OpenFeature.",
|
||||
Long: `Code generation for flag accessors for OpenFeature.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Add subcommands.
|
||||
Root.AddCommand(golang.Cmd)
|
||||
Root.AddCommand(react.Cmd)
|
||||
|
||||
// Add flags.
|
||||
Root.PersistentFlags().String(flagkeys.FlagManifestPath, "", "Path to the flag manifest.")
|
||||
Root.MarkPersistentFlagRequired(flagkeys.FlagManifestPath)
|
||||
viper.BindPFlag(flagkeys.FlagManifestPath, Root.PersistentFlags().Lookup(flagkeys.FlagManifestPath))
|
||||
Root.PersistentFlags().String(flagkeys.OutputPath, "", "Output path for the codegen")
|
||||
viper.BindPFlag(flagkeys.OutputPath, Root.PersistentFlags().Lookup(flagkeys.OutputPath))
|
||||
Root.MarkPersistentFlagRequired(flagkeys.OutputPath)
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestGenerateGoSuccess(t *testing.T) {
|
||||
// Constant paths.
|
||||
const memoryManifestPath = "manifest/path.json"
|
||||
const memoryOutputPath = "output/path.go"
|
||||
const packageName = "testpackage"
|
||||
const testFileManifest = "testdata/success_manifest.golden"
|
||||
const testFileGo = "testdata/success_go.golden"
|
||||
|
||||
// Prepare in-memory files.
|
||||
fs := afero.NewMemMapFs()
|
||||
viper.Set(flagkeys.FileSystem, fs)
|
||||
readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs)
|
||||
|
||||
// Prepare command.
|
||||
Root.SetArgs([]string{"go",
|
||||
"--flag_manifest_path", memoryManifestPath,
|
||||
"--output_path", memoryOutputPath,
|
||||
"--package_name", packageName,
|
||||
})
|
||||
|
||||
// Run command.
|
||||
Root.Execute()
|
||||
|
||||
// Compare result.
|
||||
compareOutput(t, testFileGo, memoryOutputPath, fs)
|
||||
}
|
||||
|
||||
func TestGenerateReactSuccess(t *testing.T) {
|
||||
// Constant paths.
|
||||
const memoryManifestPath = "manifest/path.json"
|
||||
const memoryOutputPath = "output/path.ts"
|
||||
const testFileManifest = "testdata/success_manifest.golden"
|
||||
const testFileReact = "testdata/success_react.golden"
|
||||
|
||||
// Prepare in-memory files.
|
||||
fs := afero.NewMemMapFs()
|
||||
viper.Set(flagkeys.FileSystem, fs)
|
||||
readOsFileAndWriteToMemMap(t, testFileManifest, memoryManifestPath, fs)
|
||||
|
||||
// Prepare command.
|
||||
Root.SetArgs([]string{"react",
|
||||
"--flag_manifest_path", memoryManifestPath,
|
||||
"--output_path", memoryOutputPath,
|
||||
})
|
||||
|
||||
// Run command.
|
||||
Root.Execute()
|
||||
|
||||
// Compare result.
|
||||
compareOutput(t, testFileReact, memoryOutputPath, fs)
|
||||
}
|
||||
|
||||
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
|
||||
data, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading file %q: %v", inputPath, err)
|
||||
}
|
||||
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
|
||||
t.Fatalf("error creating directory %q: %v", filepath.Dir(memPath), err)
|
||||
}
|
||||
f, err := memFs.Create(memPath)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating file %q: %v", memPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
writtenBytes, err := f.Write(data)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing contents to file %q: %v", memPath, err)
|
||||
}
|
||||
if writtenBytes != len(data) {
|
||||
t.Fatalf("error writing entire file %v: writtenBytes != expectedWrittenBytes", memPath)
|
||||
}
|
||||
}
|
||||
|
||||
func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs) {
|
||||
want, err := os.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading file %q: %v", testFile, err)
|
||||
|
||||
}
|
||||
got, err := afero.ReadFile(fs, memoryOutputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("output mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package golang
|
||||
|
||||
import (
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
"github.com/open-feature/cli/internal/generate"
|
||||
"github.com/open-feature/cli/internal/generate/plugins/golang"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Cmd for `generate“ command, handling code generation for flag accessors
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "go",
|
||||
Short: "Generate Golang flag accessors for OpenFeature.",
|
||||
Long: `Generate Golang flag accessors for OpenFeature.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
params := golang.Params{
|
||||
GoPackage: viper.GetString(flagkeys.GoPackageName),
|
||||
}
|
||||
gen := golang.NewGenerator(params)
|
||||
err := generate.CreateFlagAccessors(gen)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().String(flagkeys.GoPackageName, "", "Name of the Go package to be generated.")
|
||||
Cmd.MarkFlagRequired(flagkeys.GoPackageName)
|
||||
viper.BindPFlag(flagkeys.GoPackageName, Cmd.Flags().Lookup(flagkeys.GoPackageName))
|
||||
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package react
|
||||
|
||||
import (
|
||||
"github.com/open-feature/cli/internal/generate"
|
||||
"github.com/open-feature/cli/internal/generate/plugins/react"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Cmd for "generate" command, handling code generation for flag accessors
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "react",
|
||||
Short: "Generate typesafe React Hooks.",
|
||||
Long: `Generate typesafe React Hooks compatible with the OpenFeature React SDK.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
params := react.Params{}
|
||||
gen := react.NewGenerator(params)
|
||||
err := generate.CreateFlagAccessors(gen)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
61
cmd/root.go
61
cmd/root.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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"))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
33
go.mod
|
|
@ -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
129
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,16 +2,65 @@
|
|||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var viperKey = "filesystem"
|
||||
|
||||
// Get the filesystem interface from the viper configuration.
|
||||
// If the filesystem interface is not set, the default filesystem interface is returned.
|
||||
func FileSystem() afero.Fs {
|
||||
return viper.Get(flagkeys.FileSystem).(afero.Fs)
|
||||
return viper.Get(viperKey).(afero.Fs)
|
||||
}
|
||||
|
||||
// Set the filesystem interface in the viper configuration.
|
||||
// This is useful for testing purposes.
|
||||
func SetFileSystem(fs afero.Fs) {
|
||||
viper.Set(viperKey, fs)
|
||||
}
|
||||
|
||||
// Writes data to a file at the given path using the filesystem interface.
|
||||
// If the file does not exist, it will be created, including all necessary directories.
|
||||
// If the file exists, it will be overwritten.
|
||||
func WriteFile(path string, data []byte) error {
|
||||
fs := FileSystem()
|
||||
if err := fs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := fs.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file %q: %v", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
writtenBytes, err := f.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing contents to file %q: %v", path, err)
|
||||
}
|
||||
if writtenBytes != len(data) {
|
||||
return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks if a file exists at the given path using the filesystem interface.
|
||||
func Exists(path string) (bool, error) {
|
||||
fs := FileSystem()
|
||||
_, err := fs.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
viper.SetDefault(flagkeys.FileSystem, afero.NewOsFs())
|
||||
viper.SetDefault(viperKey, afero.NewOsFs())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
// Package commonflags contains keys for all command-line flags related to openfeature CLI.
|
||||
package flagkeys
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
const (
|
||||
// `generate` flags:
|
||||
// FlagManifestPath is the key for the flag that stores the flag manifest path.
|
||||
FlagManifestPath = "flag_manifest_path"
|
||||
// OutputPath is the key for the flag that stores the output path.
|
||||
OutputPath = "output_path"
|
||||
|
||||
// `generate go` flags:
|
||||
// GoPackageName is the key for the flag that stores the Golang package name.
|
||||
GoPackageName = "package_name"
|
||||
|
||||
//internal keys:
|
||||
// FileSystem is the key for the flag that stores the filesystem interface.
|
||||
FileSystem = "filesystem"
|
||||
)
|
||||
|
||||
func init() {
|
||||
viper.SetDefault(FileSystem, "local")
|
||||
}
|
||||
|
|
@ -0,0 +1,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
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
// Package generate contains the top level functions used for generating flag accessors.
|
||||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/filesystem"
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
"github.com/open-feature/cli/internal/generate/manifestutils"
|
||||
"github.com/open-feature/cli/internal/generate/types"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// GenerateFile receives data for the Go template engine and outputs the contents to the file.
|
||||
// Intended to be invoked by each language generator with appropriate data.
|
||||
func GenerateFile(funcs template.FuncMap, contents string, data types.TmplDataInterface) error {
|
||||
contentsTmpl, err := template.New("contents").Funcs(funcs).Parse(contents)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing template: %v", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := contentsTmpl.Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("error executing template: %v", err)
|
||||
}
|
||||
outputPath := data.BaseTmplDataInfo().OutputPath
|
||||
fs := filesystem.FileSystem()
|
||||
if err := fs.MkdirAll(filepath.Dir(outputPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := fs.Create(path.Join(outputPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating file %q: %v", outputPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
writtenBytes, err := f.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing contents to file %q: %v", outputPath, err)
|
||||
}
|
||||
if writtenBytes != buf.Len() {
|
||||
return fmt.Errorf("error writing entire file %v: writtenBytes != expectedWrittenBytes", outputPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Takes as input a generator and outputs file with the appropriate flag accessors.
|
||||
// The flag data is taken from the provided flag manifest.
|
||||
func CreateFlagAccessors(gen types.Generator) error {
|
||||
bt, err := manifestutils.LoadData(viper.GetString(flagkeys.FlagManifestPath), gen.SupportedFlagTypes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading flag manifest: %v", err)
|
||||
}
|
||||
input := types.Input{
|
||||
BaseData: bt,
|
||||
}
|
||||
return gen.Generate(input)
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
// Package manifestutils contains useful functions for loading the flag manifest.
|
||||
package manifestutils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/open-feature/cli/internal/filesystem"
|
||||
"github.com/open-feature/cli/internal/flagkeys"
|
||||
"github.com/open-feature/cli/internal/generate/types"
|
||||
flagmanifest "github.com/open-feature/cli/schema/v0"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// LoadData loads the data from the flag manifest.
|
||||
func LoadData(manifestPath string, supportedFlagTypes map[types.FlagType]bool) (*types.BaseTmplData, error) {
|
||||
fs := filesystem.FileSystem()
|
||||
data, err := afero.ReadFile(fs, manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading contents from file %q", manifestPath)
|
||||
}
|
||||
unfilteredData, err := unmarshalFlagManifest(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredData := filterUnsupportedFlags(unfilteredData, supportedFlagTypes)
|
||||
|
||||
return filteredData, nil
|
||||
}
|
||||
|
||||
func filterUnsupportedFlags(unfilteredData *types.BaseTmplData, supportedFlagTypes map[types.FlagType]bool) *types.BaseTmplData {
|
||||
filteredData := &types.BaseTmplData{
|
||||
OutputPath: unfilteredData.OutputPath,
|
||||
}
|
||||
for _, flagData := range unfilteredData.Flags {
|
||||
if supportedFlagTypes[flagData.Type] {
|
||||
filteredData.Flags = append(filteredData.Flags, flagData)
|
||||
}
|
||||
}
|
||||
return filteredData
|
||||
}
|
||||
|
||||
var stringToFlagType = map[string]types.FlagType{
|
||||
"string": types.StringType,
|
||||
"boolean": types.BoolType,
|
||||
"float": types.FloatType,
|
||||
"integer": types.IntType,
|
||||
"object": types.ObjectType,
|
||||
}
|
||||
|
||||
func getDefaultValue(defaultValue interface{}, flagType types.FlagType) string {
|
||||
switch flagType {
|
||||
case types.BoolType:
|
||||
return strconv.FormatBool(defaultValue.(bool))
|
||||
case types.IntType:
|
||||
//the conversion to float64 instead of integer typically occurs
|
||||
//due to how JSON is parsed in Go. In Go's encoding/json package,
|
||||
//all JSON numbers are unmarshaled into float64 by default when decoding into an interface{}.
|
||||
return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64)
|
||||
case types.FloatType:
|
||||
return strconv.FormatFloat(defaultValue.(float64), 'g', -1, 64)
|
||||
case types.StringType:
|
||||
return defaultValue.(string)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalFlagManifest(data []byte) (*types.BaseTmplData, error) {
|
||||
dynamic := make(map[string]interface{})
|
||||
err := json.Unmarshal(data, &dynamic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling JSON: %v", err)
|
||||
}
|
||||
|
||||
sch, err := jsonschema.CompileString(flagmanifest.SchemaPath, flagmanifest.Schema)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error compiling JSON schema: %v", err)
|
||||
}
|
||||
if err = sch.Validate(dynamic); err != nil {
|
||||
return nil, fmt.Errorf("error validating JSON schema: %v", err)
|
||||
}
|
||||
// All casts can be done directly since the JSON is already validated by the schema.
|
||||
iFlags := dynamic["flags"]
|
||||
flags := iFlags.(map[string]interface{})
|
||||
btData := types.BaseTmplData{
|
||||
OutputPath: viper.GetString(flagkeys.OutputPath),
|
||||
}
|
||||
for flagKey, iFlagData := range flags {
|
||||
flagData := iFlagData.(map[string]interface{})
|
||||
flagTypeString := flagData["flagType"].(string)
|
||||
flagType := stringToFlagType[flagTypeString]
|
||||
docs := flagData["description"].(string)
|
||||
defaultValue := getDefaultValue(flagData["defaultValue"], flagType)
|
||||
btData.Flags = append(btData.Flags, &types.FlagTmplData{
|
||||
Name: flagKey,
|
||||
Type: flagType,
|
||||
DefaultValue: defaultValue,
|
||||
Docs: docs,
|
||||
})
|
||||
}
|
||||
// Ensure consistency of order of flag generation.
|
||||
sort.Slice(btData.Flags, func(i, j int) bool {
|
||||
return btData.Flags[i].Name < btData.Flags[j].Name
|
||||
})
|
||||
return &btData, nil
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
package golang
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/generate"
|
||||
"github.com/open-feature/cli/internal/generate/types"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
)
|
||||
|
||||
// TmplData contains the Golang-specific data and the base data for the codegen.
|
||||
type TmplData struct {
|
||||
*types.BaseTmplData
|
||||
GoPackage string
|
||||
}
|
||||
|
||||
type genImpl struct {
|
||||
goPackage string
|
||||
}
|
||||
|
||||
// BaseTmplDataInfo provides the base template data for the codegen.
|
||||
func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData {
|
||||
return td.BaseTmplData
|
||||
}
|
||||
|
||||
// supportedFlagTypes is the flag types supported by the Go template.
|
||||
var supportedFlagTypes = map[types.FlagType]bool{
|
||||
types.FloatType: true,
|
||||
types.StringType: true,
|
||||
types.IntType: true,
|
||||
types.BoolType: true,
|
||||
types.ObjectType: false,
|
||||
}
|
||||
|
||||
func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool {
|
||||
return supportedFlagTypes
|
||||
}
|
||||
|
||||
//go:embed golang.tmpl
|
||||
var golangTmpl string
|
||||
|
||||
// Go Funcs BEGIN
|
||||
|
||||
func flagVarName(flagName string) string {
|
||||
return strcase.ToCamel(flagName)
|
||||
}
|
||||
|
||||
func flagInitParam(flagName string) string {
|
||||
return strconv.Quote(flagName)
|
||||
}
|
||||
|
||||
func openFeatureType(t types.FlagType) string {
|
||||
switch t {
|
||||
case types.IntType:
|
||||
return "Int"
|
||||
case types.FloatType:
|
||||
return "Float"
|
||||
case types.BoolType:
|
||||
return "Boolean"
|
||||
case types.StringType:
|
||||
return "String"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func supportImports(flags []*types.FlagTmplData) []string {
|
||||
var res []string
|
||||
if len(flags) > 0 {
|
||||
res = append(res, "\"context\"")
|
||||
res = append(res, "\"github.com/open-feature/go-sdk/openfeature\"")
|
||||
}
|
||||
sort.Strings(res)
|
||||
return res
|
||||
}
|
||||
|
||||
func defaultValueLiteral(flag *types.FlagTmplData) string {
|
||||
switch flag.Type {
|
||||
case types.StringType:
|
||||
return strconv.Quote(flag.DefaultValue)
|
||||
default:
|
||||
return flag.DefaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func typeString(flagType types.FlagType) string {
|
||||
switch flagType {
|
||||
case types.StringType:
|
||||
return "string"
|
||||
case types.IntType:
|
||||
return "int64"
|
||||
case types.BoolType:
|
||||
return "bool"
|
||||
case types.FloatType:
|
||||
return "float64"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Go Funcs END
|
||||
|
||||
// Generate generates the Go flag accessors for OpenFeature.
|
||||
func (g *genImpl) Generate(input types.Input) error {
|
||||
funcs := template.FuncMap{
|
||||
"FlagVarName": flagVarName,
|
||||
"FlagInitParam": flagInitParam,
|
||||
"OpenFeatureType": openFeatureType,
|
||||
"SupportImports": supportImports,
|
||||
"DefaultValueLiteral": defaultValueLiteral,
|
||||
"TypeString": typeString,
|
||||
}
|
||||
td := TmplData{
|
||||
BaseTmplData: input.BaseData,
|
||||
GoPackage: g.goPackage,
|
||||
}
|
||||
return generate.GenerateFile(funcs, golangTmpl, &td)
|
||||
}
|
||||
|
||||
// Params are parameters for creating a Generator
|
||||
type Params struct {
|
||||
GoPackage string
|
||||
}
|
||||
|
||||
// NewGenerator creates a generator for Go.
|
||||
func NewGenerator(params Params) types.Generator {
|
||||
return &genImpl{
|
||||
goPackage: params.GoPackage,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
package react
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-feature/cli/internal/generate"
|
||||
"github.com/open-feature/cli/internal/generate/types"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
)
|
||||
|
||||
type TmplData struct {
|
||||
*types.BaseTmplData
|
||||
}
|
||||
|
||||
type genImpl struct {
|
||||
}
|
||||
|
||||
// BaseTmplDataInfo provides the base template data for the codegen.
|
||||
func (td *TmplData) BaseTmplDataInfo() *types.BaseTmplData {
|
||||
return td.BaseTmplData
|
||||
}
|
||||
|
||||
// supportedFlagTypes is the flag types supported by the Go template.
|
||||
var supportedFlagTypes = map[types.FlagType]bool{
|
||||
types.FloatType: true,
|
||||
types.StringType: true,
|
||||
types.IntType: true,
|
||||
types.BoolType: true,
|
||||
types.ObjectType: false,
|
||||
}
|
||||
|
||||
func (*genImpl) SupportedFlagTypes() map[types.FlagType]bool {
|
||||
return supportedFlagTypes
|
||||
}
|
||||
|
||||
//go:embed react.tmpl
|
||||
var reactTmpl string
|
||||
|
||||
func flagVarName(flagName string) string {
|
||||
return strcase.ToCamel(flagName)
|
||||
}
|
||||
|
||||
func flagInitParam(flagName string) string {
|
||||
return strconv.Quote(flagName)
|
||||
}
|
||||
|
||||
func defaultValueLiteral(flag *types.FlagTmplData) string {
|
||||
switch flag.Type {
|
||||
case types.StringType:
|
||||
return strconv.Quote(flag.DefaultValue)
|
||||
default:
|
||||
return flag.DefaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func typeString(flagType types.FlagType) string {
|
||||
switch flagType {
|
||||
case types.StringType:
|
||||
return "string"
|
||||
case types.IntType, types.FloatType:
|
||||
return "number"
|
||||
case types.BoolType:
|
||||
return "boolean"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (g *genImpl) Generate(input types.Input) error {
|
||||
funcs := template.FuncMap{
|
||||
"FlagVarName": flagVarName,
|
||||
"FlagInitParam": flagInitParam,
|
||||
"DefaultValueLiteral": defaultValueLiteral,
|
||||
"TypeString": typeString,
|
||||
}
|
||||
td := TmplData{
|
||||
BaseTmplData: input.BaseData,
|
||||
}
|
||||
return generate.GenerateFile(funcs, reactTmpl, &td)
|
||||
}
|
||||
|
||||
// Params are parameters for creating a Generator
|
||||
type Params struct {
|
||||
}
|
||||
|
||||
// NewGenerator creates a generator for React.
|
||||
func NewGenerator(params Params) types.Generator {
|
||||
return &genImpl{}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
type ReactFlagEvaluationOptions,
|
||||
type ReactFlagEvaluationNoSuspenseOptions,
|
||||
useFlag,
|
||||
useSuspenseFlag,
|
||||
} from "@openfeature/react-sdk";
|
||||
{{ range .Flags}}
|
||||
/**
|
||||
* {{.Docs}}
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Name}}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{TypeString .Type}}`
|
||||
*/
|
||||
export const use{{FlagVarName .Name}} = (options: ReactFlagEvaluationOptions) => {
|
||||
return useFlag({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* {{.Docs}}
|
||||
*
|
||||
* **Details:**
|
||||
* - flag key: `{{ .Name}}`
|
||||
* - default value: `{{ .DefaultValue }}`
|
||||
* - type: `{{TypeString .Type}}`
|
||||
*
|
||||
* Equivalent to useFlag with options: `{ suspend: true }`
|
||||
* @experimental — Suspense is an experimental feature subject to change in future versions.
|
||||
*/
|
||||
export const useSuspense{{FlagVarName .Name}} = (options: ReactFlagEvaluationNoSuspenseOptions) => {
|
||||
return useSuspenseFlag({{FlagInitParam .Name}}, {{DefaultValueLiteral .}}, options);
|
||||
};
|
||||
{{ end}}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// Package types contains all the common types and interfaces for generating flag accessors.
|
||||
package types
|
||||
|
||||
// FlagType are the primitive types of flags.
|
||||
type FlagType int
|
||||
|
||||
// Collection of the different kinds of flag types
|
||||
const (
|
||||
UnknownFlagType FlagType = iota
|
||||
IntType
|
||||
FloatType
|
||||
BoolType
|
||||
StringType
|
||||
ObjectType
|
||||
)
|
||||
|
||||
// FlagTmplData is the per-flag specific data.
|
||||
// It represents a common interface between Mendel source and codegen file output.
|
||||
type FlagTmplData struct {
|
||||
Name string
|
||||
Type FlagType
|
||||
DefaultValue string
|
||||
Docs string
|
||||
}
|
||||
|
||||
// BaseTmplData is the base for all OpenFeature code generation.
|
||||
type BaseTmplData struct {
|
||||
OutputPath string
|
||||
Flags []*FlagTmplData
|
||||
}
|
||||
|
||||
type TmplDataInterface interface {
|
||||
// BaseTmplDataInfo returns a pointer to a BaseTmplData struct containing
|
||||
// all the relevant information needed for metadata construction.
|
||||
BaseTmplDataInfo() *BaseTmplData
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
BaseData *BaseTmplData
|
||||
}
|
||||
|
||||
// Generator provides interface to generate language specific, strongly-typed flag accessors.
|
||||
type Generator interface {
|
||||
Generate(input Input) error
|
||||
SupportedFlagTypes() map[FlagType]bool
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package generators
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"golang.org/x/text/cases"
|
||||
)
|
||||
|
||||
func defaultFuncs() template.FuncMap {
|
||||
// Update the contributing doc when adding a new function
|
||||
return template.FuncMap{
|
||||
// Remapping ToCamel to ToPascal to match the expected behavior
|
||||
// Ref: https://github.com/iancoleman/strcase/issues/53
|
||||
"ToPascal": strcase.ToCamel,
|
||||
// Remapping ToLowerCamel to ToCamel to match the expected behavior
|
||||
// Ref: See above
|
||||
"ToCamel": strcase.ToLowerCamel,
|
||||
"ToKebab": strcase.ToKebab,
|
||||
"ToScreamingKebab": strcase.ToScreamingKebab,
|
||||
"ToSnake": strcase.ToSnake,
|
||||
"ToScreamingSnake": strcase.ToScreamingSnake,
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": cases.Title,
|
||||
"Quote": strconv.Quote,
|
||||
"QuoteString": func (input any) any {
|
||||
if str, ok := input.(string); ok {
|
||||
return strconv.Quote(str)
|
||||
}
|
||||
return input
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// results in "Api" using ToCamel("API")
|
||||
// results in "api" using ToLowerCamel("API")
|
||||
strcase.ConfigureAcronym("API", "api")
|
||||
}
|
||||
|
|
@ -0,0 +1,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())
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package generators
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GeneratorCreator is a function that creates a generator command
|
||||
type GeneratorCreator func() *cobra.Command
|
||||
|
||||
// GeneratorInfo contains metadata about a generator
|
||||
type GeneratorInfo struct {
|
||||
Name string
|
||||
Description string
|
||||
Stability Stability
|
||||
Creator GeneratorCreator
|
||||
}
|
||||
|
||||
// GeneratorManager maintains a registry of available generators
|
||||
type GeneratorManager struct {
|
||||
generators map[string]GeneratorInfo
|
||||
}
|
||||
|
||||
// NewGeneratorManager creates a new generator manager
|
||||
func NewGeneratorManager() *GeneratorManager {
|
||||
return &GeneratorManager{
|
||||
generators: make(map[string]GeneratorInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a generator to the registry
|
||||
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
|
||||
cmd := cmdCreator()
|
||||
m.generators[cmd.Use] = GeneratorInfo{
|
||||
Name: cmd.Use,
|
||||
Description: cmd.Short,
|
||||
Stability: Stability(cmd.Annotations["stability"]),
|
||||
Creator: cmdCreator,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns all registered generators
|
||||
func (m *GeneratorManager) GetAll() map[string]GeneratorInfo {
|
||||
return m.generators
|
||||
}
|
||||
|
||||
// GetCommands returns cobra commands for all registered generators
|
||||
func (m *GeneratorManager) GetCommands() []*cobra.Command {
|
||||
var commands []*cobra.Command
|
||||
|
||||
for _, info := range m.generators {
|
||||
commands = append(commands, info.Creator())
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
// PrintGeneratorsTable prints a table of all available generators with their stability
|
||||
func (m *GeneratorManager) PrintGeneratorsTable() error {
|
||||
tableData := [][]string{
|
||||
{"Generator", "Description", "Stability"},
|
||||
}
|
||||
|
||||
// Get generator names for consistent ordering
|
||||
var names []string
|
||||
for name := range m.generators {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
info := m.generators[name]
|
||||
tableData = append(tableData, []string{
|
||||
name,
|
||||
info.Description,
|
||||
string(info.Stability),
|
||||
})
|
||||
}
|
||||
|
||||
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
|
||||
}
|
||||
|
||||
// DefaultManager is the default instance of the generator manager
|
||||
var DefaultManager = NewGeneratorManager()
|
||||
|
|
@ -0,0 +1,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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -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}}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Change struct {
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
OldValue any `json:"oldValue,omitempty"`
|
||||
NewValue any `json:"newValue,omitempty"`
|
||||
}
|
||||
|
||||
func Compare(oldManifest, newManifest *Manifest) ([]Change, error) {
|
||||
var changes []Change
|
||||
oldFlags := oldManifest.Flags
|
||||
newFlags := newManifest.Flags
|
||||
|
||||
for key, newFlag := range newFlags {
|
||||
if oldFlag, exists := oldFlags[key]; exists {
|
||||
if !reflect.DeepEqual(oldFlag, newFlag) {
|
||||
changes = append(changes, Change{
|
||||
Type: "change",
|
||||
Path: fmt.Sprintf("flags.%s", key),
|
||||
OldValue: oldFlag,
|
||||
NewValue: newFlag,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
changes = append(changes, Change{
|
||||
Type: "add",
|
||||
Path: fmt.Sprintf("flags.%s", key),
|
||||
NewValue: newFlag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for key, oldFlag := range oldFlags {
|
||||
if _, exists := newFlags[key]; !exists {
|
||||
changes = append(changes, Change{
|
||||
Type: "remove",
|
||||
Path: fmt.Sprintf("flags.%s", key),
|
||||
OldValue: oldFlag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
type BooleanFlag struct {
|
||||
BaseFlag
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"enum=boolean"`
|
||||
// The value returned from an unsuccessful flag evaluation
|
||||
DefaultValue bool `json:"defaultValue,omitempty"`
|
||||
}
|
||||
|
||||
type StringFlag struct {
|
||||
BaseFlag
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"enum=string"`
|
||||
// The value returned from an unsuccessful flag evaluation
|
||||
DefaultValue string `json:"defaultValue,omitempty"`
|
||||
}
|
||||
|
||||
type IntegerFlag struct {
|
||||
BaseFlag
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"enum=integer"`
|
||||
// The value returned from an unsuccessful flag evaluation
|
||||
DefaultValue int `json:"defaultValue,omitempty"`
|
||||
}
|
||||
|
||||
type FloatFlag struct {
|
||||
BaseFlag
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"enum=float"`
|
||||
// The value returned from an unsuccessful flag evaluation
|
||||
DefaultValue float64 `json:"defaultValue,omitempty"`
|
||||
}
|
||||
|
||||
type ObjectFlag struct {
|
||||
BaseFlag
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"enum=object"`
|
||||
// The value returned from an unsuccessful flag evaluation
|
||||
DefaultValue any `json:"defaultValue,omitempty"`
|
||||
}
|
||||
|
||||
type BaseFlag struct {
|
||||
// The type of feature flag (e.g., boolean, string, integer, float)
|
||||
Type string `json:"flagType,omitempty" jsonschema:"required"`
|
||||
// A concise description of this feature flag's purpose.
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// Feature flag manifest for the OpenFeature CLI
|
||||
type Manifest struct {
|
||||
// Collection of feature flag definitions
|
||||
Flags map[string]any `json:"flags" jsonschema:"title=Flags,required"`
|
||||
}
|
||||
|
||||
// Converts the Manifest struct to a JSON schema.
|
||||
func ToJSONSchema() *jsonschema.Schema {
|
||||
reflector := &jsonschema.Reflector{
|
||||
ExpandedStruct: true,
|
||||
AllowAdditionalProperties: true,
|
||||
BaseSchemaID: "openfeature-cli",
|
||||
}
|
||||
|
||||
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
|
||||
pterm.Error.Printf("Error extracting comments from types.go: %v\n", err)
|
||||
}
|
||||
|
||||
schema := reflector.Reflect(Manifest{})
|
||||
schema.Version = "http://json-schema.org/draft-07/schema#"
|
||||
schema.Title = "OpenFeature CLI Manifest"
|
||||
flags, ok := schema.Properties.Get("flags")
|
||||
if !ok {
|
||||
log.Fatal("flags not found")
|
||||
}
|
||||
flags.PatternProperties = map[string]*jsonschema.Schema{
|
||||
"^.{1,}$": {
|
||||
Ref: "#/$defs/flag",
|
||||
},
|
||||
}
|
||||
// We only want flags keys that matches the pattern properties
|
||||
flags.AdditionalProperties = jsonschema.FalseSchema
|
||||
|
||||
schema.Definitions = jsonschema.Definitions{
|
||||
"flag": &jsonschema.Schema{
|
||||
OneOf: []*jsonschema.Schema{
|
||||
{Ref: "#/$defs/booleanFlag"},
|
||||
{Ref: "#/$defs/stringFlag"},
|
||||
{Ref: "#/$defs/integerFlag"},
|
||||
{Ref: "#/$defs/floatFlag"},
|
||||
{Ref: "#/$defs/objectFlag"},
|
||||
},
|
||||
Required: []string{"flagType", "defaultValue"},
|
||||
},
|
||||
"booleanFlag": &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: reflector.Reflect(BooleanFlag{}).Properties,
|
||||
},
|
||||
"stringFlag": &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: reflector.Reflect(StringFlag{}).Properties,
|
||||
},
|
||||
"integerFlag": &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: reflector.Reflect(IntegerFlag{}).Properties,
|
||||
},
|
||||
"floatFlag": &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: reflector.Reflect(FloatFlag{}).Properties,
|
||||
},
|
||||
"objectFlag": &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: reflector.Reflect(ObjectFlag{}).Properties,
|
||||
},
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema/v0/flag-manifest.json",
|
||||
"flags": {
|
||||
"enableFeatureA": {
|
||||
"flagType": "boolean",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/open-feature/cli/internal/manifest"
|
||||
)
|
||||
|
||||
const schemaPath = "schema/v0/flag-manifest.json"
|
||||
|
||||
func main() {
|
||||
schema := manifest.ToJSONSchema()
|
||||
data, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to marshal JSON schema: %w", err))
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("schema/v0", os.ModePerm); err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to create directory: %w", err))
|
||||
}
|
||||
|
||||
file, err := os.Create(schemaPath)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to create file: %w", err))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(data); err != nil {
|
||||
log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err));
|
||||
}
|
||||
|
||||
fmt.Println("JSON schema generated successfully at " + schemaPath)
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "openfeature-cli/manifest",
|
||||
"$defs": {
|
||||
"booleanFlag": {
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"boolean"
|
||||
],
|
||||
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A concise description of this feature flag's purpose."
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "boolean",
|
||||
"description": "The value returned from an unsuccessful flag evaluation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"flag": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/booleanFlag"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/stringFlag"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/integerFlag"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/floatFlag"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/objectFlag"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"flagType",
|
||||
"defaultValue"
|
||||
]
|
||||
},
|
||||
"floatFlag": {
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"float"
|
||||
],
|
||||
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A concise description of this feature flag's purpose."
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "number",
|
||||
"description": "The value returned from an unsuccessful flag evaluation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"integerFlag": {
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"integer"
|
||||
],
|
||||
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A concise description of this feature flag's purpose."
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "integer",
|
||||
"description": "The value returned from an unsuccessful flag evaluation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"objectFlag": {
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"object"
|
||||
],
|
||||
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A concise description of this feature flag's purpose."
|
||||
},
|
||||
"defaultValue": {
|
||||
"description": "The value returned from an unsuccessful flag evaluation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"stringFlag": {
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string"
|
||||
],
|
||||
"description": "The type of feature flag (e.g., boolean, string, integer, float)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A concise description of this feature flag's purpose."
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "string",
|
||||
"description": "The value returned from an unsuccessful flag evaluation"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"flags": {
|
||||
"patternProperties": {
|
||||
"^.{1,}$": {
|
||||
"$ref": "#/$defs/flag"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"title": "Flags",
|
||||
"description": "Collection of feature flag definitions"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"flags"
|
||||
],
|
||||
"title": "OpenFeature CLI Manifest",
|
||||
"description": "Feature flag manifest for the OpenFeature CLI"
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
{
|
||||
"$id": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag_manifest.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Flag Manifest",
|
||||
"description": "Describes a configuration of OpenFeature flags, including info such as their types and default values.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flags": {
|
||||
"description": "Object containing the flags in the config",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.{1,}$": {
|
||||
"description": "The definition of one flag",
|
||||
"$ref": "#/$defs/flag"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"flags"
|
||||
],
|
||||
"$defs": {
|
||||
"flag": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/booleanType"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/stringType"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/integerType"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/floatType"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/objectType"
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"flagType",
|
||||
"defaultValue"
|
||||
]
|
||||
},
|
||||
"booleanType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"boolean"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"description": "The default value returned in code if a flag evaluation is unsuccessful",
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stringType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integerType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"integer"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"floatType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"float"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"objectType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flagType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"object"
|
||||
]
|
||||
},
|
||||
"defaultValue": {
|
||||
"type": "object"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../../flag_manifest.json",
|
||||
"$schema": "../../flag-manifest.json",
|
||||
"flags": {
|
||||
"booleanFlag": {
|
||||
"codeDefault": true
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../../flag_manifest.json",
|
||||
"$schema": "../../flag-manifest.json",
|
||||
"flags": {
|
||||
"booleanFlag": {
|
||||
"flagType": "boolean",
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"$schema": "../../flag_manifest.json",
|
||||
"flags": {}
|
||||
}
|
||||
Loading…
Reference in New Issue