Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.dll
*.so
*.dylib
cli

# Test binary, built with `go test -c`
*.test
Expand All @@ -25,5 +26,6 @@ go.work.sum
.env
dist

flags.json
# openfeature cli config
.openfeature.yaml
.openfeature.yaml
6 changes: 3 additions & 3 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ 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/open-feature/cli/internal/manifest"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -53,7 +53,7 @@ func GetGenerateReactCmd() *cobra.Command {
OutputPath: outputPath,
Custom: react.Params{},
}
flagset, err := flagset.Load(manifestPath)
flagset, err := manifest.LoadFlagSet(manifestPath)
if err != nil {
return err
}
Expand Down Expand Up @@ -96,7 +96,7 @@ func GetGenerateGoCmd() *cobra.Command {
},
}

flagset, err := flagset.Load(manifestPath)
flagset, err := manifest.LoadFlagSet(manifestPath)
if err != nil {
return err
}
Expand Down
18 changes: 18 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func GetInitCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
override := config.GetOverride(cmd)
flagSourceUrl := config.GetFlagSourceUrl(cmd)

manifestExists, _ := filesystem.Exists(manifestPath)
if (manifestExists && !override) {
Expand All @@ -39,6 +40,23 @@ func GetInitCmd() *cobra.Command {
if err != nil {
return err
}

configFileExists, _ := filesystem.Exists(".openfeature.yaml")
if !configFileExists {
err = filesystem.WriteFile(".openfeature.yaml", []byte(""))
if err != nil {
return err
}
}

if flagSourceUrl != "" {
pterm.Info.Println("Writing flag source URL to .openfeature.yaml", pterm.LightWhite(flagSourceUrl))
err = filesystem.WriteFile(".openfeature.yaml", []byte("flagSourceUrl: " + flagSourceUrl))
if err != nil {
return err
}
}

pterm.Info.Printfln("Manifest created at %s", pterm.LightWhite(manifestPath))
pterm.Success.Println("Project initialized.")
return nil
Expand Down
106 changes: 106 additions & 0 deletions cmd/pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"errors"
"fmt"
"strconv"

"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/filesystem"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/manifest"
"github.com/open-feature/cli/internal/requests"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

func promptForDefaultValue(flag *flagset.Flag) (any) {
var prompt string
switch flag.Type {
case flagset.BoolType:
var options []string = []string{"false", "true"}
prompt = fmt.Sprintf("Enter default value for flag '%s' (%s)", flag.Key, flag.Type)
boolStr, _ := pterm.DefaultInteractiveSelect.WithOptions(options).WithFilter(false).Show(prompt)
boolValue, _ := strconv.ParseBool(boolStr)
return boolValue
case flagset.IntType:
var err error = errors.New("Input a valid integer")
prompt = fmt.Sprintf("Enter default value for flag '%s' (%s)", flag.Key, flag.Type)
var defaultValue int
for err != nil {
defaultValueString, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("0").Show(prompt)
defaultValue, err = strconv.Atoi(defaultValueString)
}
return defaultValue
case flagset.FloatType:
var err error = errors.New("Input a valid float")
prompt = fmt.Sprintf("Enter default value for flag '%s' (%s)", flag.Key, flag.Type)
var defaultValue float64
for err != nil {
defaultValueString, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("0.0").Show(prompt)
defaultValue, err = strconv.ParseFloat(defaultValueString, 64)
if err != nil {
pterm.Error.Println("Input a valid float")
}
}
return defaultValue
case flagset.StringType:
prompt = fmt.Sprintf("Enter default value for flag '%s' (%s)", flag.Key, flag.Type)
defaultValue, _ := pterm.DefaultInteractiveTextInput.WithDefaultText("").Show(prompt)
return defaultValue
// TODO: Add proper support for object type
case flagset.ObjectType:
return map[string]any{}
default:
return nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the UX here, well done 🥇

}

func GetPullCmd() *cobra.Command {
pullCmd := &cobra.Command{
Use: "pull",
Short: "Pull a flag manifest from a remote source",
Long: "Pull a flag manifest from a remote source.",
PreRunE: func(cmd *cobra.Command, args []string) error {
return initializeConfig(cmd, "pull")
},
RunE: func(cmd *cobra.Command, args []string) error {
flagSourceUrl := config.GetFlagSourceUrl(cmd)
manifestPath := config.GetManifestPath(cmd)
authToken := config.GetAuthToken(cmd)

if flagSourceUrl == "" {
url, err := filesystem.GetFromYaml("flagSourceUrl")
if err != nil {
return fmt.Errorf("error getting flagSourceUrl from config: %w", err)
}
flagSourceUrl = url
}

// fetch the flags from the remote source
flags, err := requests.FetchFlags(flagSourceUrl, authToken)
if err != nil {
return fmt.Errorf("error fetching flags: %w", err)
}

// Check each flag for null defaultValue
for index, flag := range flags.Flags {
if flag.DefaultValue == nil {
defaultValue := promptForDefaultValue(&flag)
flags.Flags[index].DefaultValue = defaultValue
}
}

pterm.Success.Printf("Successfully fetched flags from %s", flagSourceUrl)
err = manifest.Write(manifestPath, flags)
if err != nil {
return fmt.Errorf("error writing manifest: %w", err)
}

return nil
},
}

config.AddPullFlags(pullCmd)
return pullCmd
}
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ func GetRootCmd() *cobra.Command {
rootCmd.AddCommand(GetVersionCmd())
rootCmd.AddCommand(GetInitCmd())
rootCmd.AddCommand(GetGenerateCmd())
rootCmd.AddCommand(GetPullCmd())

// 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.Error.Printf("Invalid flag: %s\n", err)
pterm.Println("Run 'openfeature --help' for usage information")
return err
})
Expand Down
21 changes: 21 additions & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const (
NoInputFlagName = "no-input"
GoPackageFlagName = "package-name"
OverrideFlagName = "override"
FlagSourceUrlFlagName = "flag-source-url"
AuthTokenFlagName = "auth-token"
)

// Default values for flags
Expand Down Expand Up @@ -39,6 +41,13 @@ func AddGoGenerateFlags(cmd *cobra.Command) {
// AddInitFlags adds the init command specific flags
func AddInitFlags(cmd *cobra.Command) {
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
cmd.Flags().String(FlagSourceUrlFlagName, "", "The URL of the flag source")
}

// AddPullFlags adds the pull command specific flags
func AddPullFlags(cmd *cobra.Command) {
cmd.Flags().String(FlagSourceUrlFlagName, "", "The URL of the flag source")
cmd.Flags().String(AuthTokenFlagName, "", "The auth token for the flag source")
}

// GetManifestPath gets the manifest path from the given command
Expand Down Expand Up @@ -70,3 +79,15 @@ func GetOverride(cmd *cobra.Command) bool {
override, _ := cmd.Flags().GetBool(OverrideFlagName)
return override
}

// GetFlagSourceUrl gets the flag source URL from the given command
func GetFlagSourceUrl(cmd *cobra.Command) string {
flagSourceUrl, _ := cmd.Flags().GetString(FlagSourceUrlFlagName)
return flagSourceUrl
}

// GetAuthToken gets the auth token from the given command
func GetAuthToken(cmd *cobra.Command) string {
authToken, _ := cmd.Flags().GetString(AuthTokenFlagName)
return authToken
}
30 changes: 30 additions & 0 deletions internal/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import (

"github.com/spf13/afero"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

var viperKey = "filesystem"

type Config struct {
FlagSourceUrl string `yaml:"flagSourceUrl"`
}

// 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 {
Expand Down Expand Up @@ -48,6 +53,31 @@ func WriteFile(path string, data []byte) error {
return nil
}

func ReadFile(path string) ([]byte, error) {
fs := FileSystem()
return afero.ReadFile(fs, path)
}

func GetFromYaml(key string) (string, error) {
var config map[string]string
fs, err := ReadFile(".openfeature.yaml")
if err != nil {
return "", err
}

err = yaml.Unmarshal(fs, &config)
if err != nil {
return "", err
}

value, exists := config[key]
if !exists {
return "", fmt.Errorf("unknown key: %s", key)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could drop the switch statements if we take the map from unmarshal and do a key/value lookup on it.

Then maybe we could return with something like this:

	value, exists := config[key]
	if !exists {
		return "", fmt.Errorf("unknown key: %s", key)
	}

	return value, nil
	```

}

return value, nil
}

// Checks if a file exists at the given path using the filesystem interface.
func Exists(path string) (bool, error) {
fs := FileSystem()
Expand Down
73 changes: 44 additions & 29 deletions internal/flagset/flagset.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import (
"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.
Expand All @@ -31,7 +27,7 @@ func (f FlagType) String() string {
case FloatType:
return "float"
case BoolType:
return "bool"
return "boolean"
case StringType:
return "string"
case ObjectType:
Expand All @@ -52,29 +48,6 @@ 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
Expand Down Expand Up @@ -131,4 +104,46 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
})

return nil
}
}

func LoadFromSourceFlags(data []byte) (*[]Flag, error) {
type SourceFlag struct {
Key string `json:"key"`
Type string `json:"type"`
Description string `json:"description"`
DefaultValue any `json:"defaultValue"`
}

var sourceFlags []SourceFlag
if err := json.Unmarshal(data, &sourceFlags); err != nil {
return nil, err
}

var flags []Flag
for _, sf := range sourceFlags {
var flagType FlagType
switch sf.Type {
case "integer", "Integer":
flagType = IntType
case "float", "Float", "Number":
flagType = FloatType
case "boolean", "bool", "Boolean":
flagType = BoolType
case "string", "String":
flagType = StringType
case "object", "Object", "JSON":
flagType = ObjectType
default:
return nil, fmt.Errorf("unknown flag type: %s", sf.Type)
}

flags = append(flags, Flag{
Key: sf.Key,
Type: flagType,
Description: sf.Description,
DefaultValue: sf.DefaultValue,
})
}

return &flags, nil
}
Loading