Skip to content

Commit 106bf9d

Browse files
authored
refactor!: add init command, update cli flags, support a config file (open-feature#71)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
1 parent e430a8d commit 106bf9d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2181
-1048
lines changed

.github/workflows/pr-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ jobs:
2727
- name: golangci-lint
2828
uses: golangci/golangci-lint-action@v6
2929
with:
30-
version: v1.60
30+
version: v1.64
3131
only-new-issues: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ go.work.sum
2424
# env file
2525
.env
2626
dist
27+
28+
# openfeature cli config
29+
.openfeature.yaml

CONTRIBUTING.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
TODO: Add contributing guidelines
2+
3+
## Templates
4+
5+
### Data
6+
7+
The `TemplateData` struct is used to pass data to the templates.
8+
9+
### Built-in template functions
10+
11+
The following functions are automatically included in the templates:
12+
13+
#### ToPascal
14+
15+
Converts a string to `PascalCase`
16+
17+
```go
18+
{{ "hello world" | ToPascal }} // HelloWorld
19+
```
20+
21+
#### ToCamel
22+
23+
Converts a string to `camelCase`
24+
25+
```go
26+
{{ "hello world" | ToCamel }} // helloWorld
27+
```
28+
29+
#### ToKebab
30+
31+
Converts a string to `kebab-case`
32+
33+
```go
34+
{{ "hello world" | ToKebab }} // hello-world
35+
```
36+
37+
#### ToSnake
38+
39+
Converts a string to `snake_case`
40+
41+
```go
42+
{{ "hello world" | ToSnake }} // hello_world
43+
```
44+
45+
#### ToScreamingSnake
46+
47+
Converts a string to `SCREAMING_SNAKE_CASE`
48+
49+
```go
50+
{{ "hello world" | ToScreamingSnake }} // HELLO_WORLD
51+
```
52+
53+
#### ToUpper
54+
55+
Converts a string to `UPPER CASE`
56+
57+
```go
58+
{{ "hello world" | ToUpper }} // HELLO WORLD
59+
```
60+
61+
#### ToLower
62+
63+
Converts a string to `lower case`
64+
65+
```go
66+
{{ "HELLO WORLD" | ToLower }} // hello world
67+
```
68+
69+
#### ToTitle
70+
71+
Converts a string to `Title Case`
72+
73+
```go
74+
{{ "hello world" | ToTitle }} // Hello World
75+
```
76+
77+
#### Quote
78+
79+
Wraps a string in double quotes
80+
81+
```go
82+
{{ "hello world" | Quote }} // "hello world"
83+
```
84+
85+
#### QuoteString
86+
87+
Wraps only strings in double quotes
88+
89+
```go
90+
{{ "hello world" | QuoteString }} // "hello world"
91+
{{ 123 | QuoteString }} // 123
92+
```
93+
94+
### Custom template functions
95+
96+
You can add custom template functions by passing a `FuncMap` to the `GenerateFile` function.

Makefile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
.PHONY: test
33
test:
44
@echo "Running tests..."
5-
go test -v ./...
5+
@go test -v ./...
66
@echo "Tests passed successfully!"
77

88
generate-docs:
99
@echo "Generating documentation..."
10-
go run ./docs/generate-commands.go
11-
@echo "Documentation generated successfully!"
10+
@go run ./docs/generate-commands.go
11+
@echo "Documentation generated successfully!"
12+
13+
generate-schema:
14+
@echo "Generating schema..."
15+
@go run ./schema/generate-schema.go
16+
@echo "Schema generated successfully!"

cmd/config.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/spf13/pflag"
9+
"github.com/spf13/viper"
10+
)
11+
12+
// initializeConfig reads in config file and ENV variables if set.
13+
// It applies configuration values to command flags based on hierarchical priority.
14+
func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
15+
v := viper.New()
16+
17+
// Set the config file name and path
18+
v.SetConfigName(".openfeature")
19+
v.AddConfigPath(".")
20+
21+
// Read the config file
22+
if err := v.ReadInConfig(); err != nil {
23+
// It's okay if there isn't a config file
24+
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
25+
return err
26+
}
27+
}
28+
29+
// Track which flags were set directly via command line
30+
cmdLineFlags := make(map[string]bool)
31+
cmd.Flags().Visit(func(f *pflag.Flag) {
32+
cmdLineFlags[f.Name] = true
33+
})
34+
35+
// Apply the configuration values
36+
cmd.Flags().VisitAll(func(f *pflag.Flag) {
37+
// Skip if flag was set on command line
38+
if cmdLineFlags[f.Name] {
39+
return
40+
}
41+
42+
// Build configuration paths from most specific to least specific
43+
configPaths := []string{}
44+
45+
// Check the most specific path (e.g., generate.go.package-name)
46+
if bindPrefix != "" {
47+
configPaths = append(configPaths, bindPrefix + "." + f.Name)
48+
49+
// Check parent paths (e.g., generate.package-name)
50+
parts := strings.Split(bindPrefix, ".")
51+
for i := len(parts) - 1; i > 0; i-- {
52+
parentPath := strings.Join(parts[:i], ".") + "." + f.Name
53+
configPaths = append(configPaths, parentPath)
54+
}
55+
}
56+
57+
// Check the base path (e.g., package-name)
58+
configPaths = append(configPaths, f.Name)
59+
60+
// Try each path in order until we find a match
61+
for _, path := range configPaths {
62+
if v.IsSet(path) {
63+
val := v.Get(path)
64+
_ = f.Value.Set(fmt.Sprintf("%v", val))
65+
break
66+
}
67+
}
68+
})
69+
70+
return nil
71+
}

cmd/config_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func setupTestCommand() *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "test",
15+
}
16+
17+
// Add some test flags
18+
cmd.Flags().String("output", "", "output path")
19+
cmd.Flags().String("package-name", "default", "package name")
20+
21+
return cmd
22+
}
23+
24+
// setupConfigFileForTest creates a temporary directory with a config file
25+
// and changes the working directory to it.
26+
// Returns the original working directory and temp directory path for cleanup.
27+
func setupConfigFileForTest(t *testing.T, configContent string) (string, string) {
28+
// Create a temporary config file
29+
tmpDir, err := os.MkdirTemp("", "config-test")
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
configPath := filepath.Join(tmpDir, ".openfeature.yaml")
35+
err = os.WriteFile(configPath, []byte(configContent), 0644)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
40+
// Change to the temporary directory so the config file can be found
41+
originalDir, _ := os.Getwd()
42+
err = os.Chdir(tmpDir)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
return originalDir, tmpDir
48+
}
49+
50+
func TestRootCommandIgnoresUnrelatedConfig(t *testing.T) {
51+
configContent := `
52+
generate:
53+
output: output-from-generate
54+
`
55+
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
56+
defer func() {
57+
_ = os.Chdir(originalDir)
58+
_ = os.RemoveAll(tmpDir)
59+
}()
60+
61+
rootCmd := setupTestCommand()
62+
err := initializeConfig(rootCmd, "")
63+
64+
assert.NoError(t, err)
65+
assert.Equal(t, "", rootCmd.Flag("output").Value.String(),
66+
"Root command should not get output config from unrelated sections")
67+
}
68+
69+
func TestGenerateCommandGetsGenerateConfig(t *testing.T) {
70+
configContent := `
71+
generate:
72+
output: output-from-generate
73+
`
74+
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
75+
defer func() {
76+
_ = os.Chdir(originalDir)
77+
_ = os.RemoveAll(tmpDir)
78+
}()
79+
80+
generateCmd := setupTestCommand()
81+
err := initializeConfig(generateCmd, "generate")
82+
83+
assert.NoError(t, err)
84+
assert.Equal(t, "output-from-generate", generateCmd.Flag("output").Value.String(),
85+
"Generate command should get generate.output value")
86+
}
87+
88+
func TestSubcommandGetsSpecificConfig(t *testing.T) {
89+
configContent := `
90+
generate:
91+
output: output-from-generate
92+
go:
93+
output: output-from-go
94+
package-name: fromconfig
95+
`
96+
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
97+
defer func() {
98+
_ = os.Chdir(originalDir)
99+
_ = os.RemoveAll(tmpDir)
100+
}()
101+
102+
goCmd := setupTestCommand()
103+
err := initializeConfig(goCmd, "generate.go")
104+
105+
assert.NoError(t, err)
106+
assert.Equal(t, "output-from-go", goCmd.Flag("output").Value.String(),
107+
"Go command should get generate.go.output, not generate.output")
108+
assert.Equal(t, "fromconfig", goCmd.Flag("package-name").Value.String(),
109+
"Go command should get generate.go.package-name")
110+
}
111+
112+
func TestSubcommandInheritsFromParent(t *testing.T) {
113+
configContent := `
114+
generate:
115+
output: output-from-generate
116+
`
117+
originalDir, tmpDir := setupConfigFileForTest(t, configContent)
118+
defer func() {
119+
_ = os.Chdir(originalDir)
120+
_ = os.RemoveAll(tmpDir)
121+
}()
122+
123+
otherCmd := setupTestCommand()
124+
err := initializeConfig(otherCmd, "generate.other")
125+
126+
assert.NoError(t, err)
127+
assert.Equal(t, "output-from-generate", otherCmd.Flag("output").Value.String(),
128+
"Other command should inherit generate.output when no specific config exists")
129+
}
130+
131+
func TestCommandLineOverridesConfig(t *testing.T) {
132+
// Create a temporary config file
133+
tmpDir, err := os.MkdirTemp("", "config-test")
134+
if err != nil {
135+
t.Fatal(err)
136+
}
137+
defer func() {
138+
_ = os.RemoveAll(tmpDir)
139+
}()
140+
141+
configPath := filepath.Join(tmpDir, ".openfeature.yaml")
142+
configContent := `
143+
generate:
144+
output: output-from-config
145+
`
146+
err = os.WriteFile(configPath, []byte(configContent), 0644)
147+
if err != nil {
148+
t.Fatal(err)
149+
}
150+
151+
// Change to the temporary directory so the config file can be found
152+
originalDir, _ := os.Getwd()
153+
err = os.Chdir(tmpDir)
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
defer func() {
158+
_ = os.Chdir(originalDir)
159+
}()
160+
161+
// Set up a command with a flag value already set via command line
162+
cmd := setupTestCommand()
163+
_ = cmd.Flags().Set("output", "output-from-cmdline")
164+
165+
// Initialize config
166+
err = initializeConfig(cmd, "generate")
167+
assert.NoError(t, err)
168+
169+
// Command line value should take precedence
170+
assert.Equal(t, "output-from-cmdline", cmd.Flag("output").Value.String(),
171+
"Command line value should override config file")
172+
}

0 commit comments

Comments
 (0)