Skip to content
Merged
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
5 changes: 5 additions & 0 deletions cmd/cli/evaluate/evaluate_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package evaluate
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"

"github.com/spf13/cobra"
"github.com/thomaspoignant/go-feature-flag/cmd/cli/helper"
rerr "github.com/thomaspoignant/go-feature-flag/cmdhelpers/err"
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/retrieverconf"
retrieverInit "github.com/thomaspoignant/go-feature-flag/cmdhelpers/retrieverconf/init"
"github.com/thomaspoignant/go-feature-flag/retriever"
Expand Down Expand Up @@ -143,6 +145,9 @@ evaluate --kind postgres --table my-table --column my-column:my-column-type --fl

err := retrieverConf.IsValid()
if err != nil {
if rcErr, ok := err.(*rerr.RetrieverConfError); ok {
return errors.New(rcErr.CliErrorMessage())
}
return err
}

Expand Down
30 changes: 30 additions & 0 deletions cmdhelpers/err/retriever_conf_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package err

import (
"fmt"
"regexp"
"strings"
)

var camelToKebabRegex = regexp.MustCompile("([a-z0-9])([A-Z])")

type RetrieverConfError struct {
property string
kind string
}

func NewRetrieverConfError(property string, kind string) *RetrieverConfError {
return &RetrieverConfError{
property: property,
kind: kind,
}
}

func (e *RetrieverConfError) Error() string {
return fmt.Sprintf("invalid retriever: no \"%s\" property found for kind \"%s\"", e.property, e.kind)
}

func (e *RetrieverConfError) CliErrorMessage() string {
kebab := camelToKebabRegex.ReplaceAllString(e.property, "${1}-${2}")
return fmt.Sprintf("invalid retriever: no \"%s\" property found for kind \"%s\"", strings.ToLower(kebab), e.kind)
}
166 changes: 166 additions & 0 deletions cmdhelpers/err/retriever_conf_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package err

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewRetrieverConfError(t *testing.T) {
tests := []struct {
name string
property string
kind string
}{
{
name: "simple property and kind",
property: "bucket",
kind: "s3",
},
{
name: "camelCase property",
property: "bucketName",
kind: "s3",
},
{
name: "empty strings",
property: "",
kind: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewRetrieverConfError(tt.property, tt.kind)
require.NotNil(t, err, "expected non-nil error")
assert.Equal(t, tt.property, err.property, "property should match")
assert.Equal(t, tt.kind, err.kind, "kind should match")
})
}
}

func TestRetrieverConfError_Error(t *testing.T) {
tests := []struct {
name string
property string
kind string
expected string
}{
{
name: "simple property and kind",
property: "bucket",
kind: "s3",
expected: `invalid retriever: no "bucket" property found for kind "s3"`,
},
{
name: "camelCase property - not converted",
property: "bucketName",
kind: "s3",
expected: `invalid retriever: no "bucketName" property found for kind "s3"`,
},
{
name: "multiple words in camelCase",
property: "someLongPropertyName",
kind: "http",
expected: `invalid retriever: no "someLongPropertyName" property found for kind "http"`,
},
{
name: "empty property and kind",
property: "",
kind: "",
expected: `invalid retriever: no "" property found for kind ""`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewRetrieverConfError(tt.property, tt.kind)
got := err.Error()
assert.Equal(t, tt.expected, got, "Error() message should match expected format")
})
}
}

func TestRetrieverConfError_CliErrorMessage(t *testing.T) {
tests := []struct {
name string
property string
kind string
expected string
}{
{
name: "simple lowercase property",
property: "bucket",
kind: "s3",
expected: `invalid retriever: no "bucket" property found for kind "s3"`,
},
{
name: "camelCase to kebab-case",
property: "bucketName",
kind: "s3",
expected: `invalid retriever: no "bucket-name" property found for kind "s3"`,
},
{
name: "multiple camelCase words",
property: "someLongPropertyName",
kind: "http",
expected: `invalid retriever: no "some-long-property-name" property found for kind "http"`,
},
{
name: "single uppercase letter",
property: "a",
kind: "test",
expected: `invalid retriever: no "a" property found for kind "test"`,
},
{
name: "uppercase property",
property: "BUCKET",
kind: "s3",
expected: `invalid retriever: no "bucket" property found for kind "s3"`,
},
{
name: "mixed case with numbers",
property: "bucket1Name",
kind: "s3",
expected: `invalid retriever: no "bucket1-name" property found for kind "s3"`,
},
{
name: "already kebab-case",
property: "bucket-name",
kind: "s3",
expected: `invalid retriever: no "bucket-name" property found for kind "s3"`,
},
{
name: "starts with uppercase",
property: "BucketName",
kind: "s3",
expected: `invalid retriever: no "bucket-name" property found for kind "s3"`,
},
{
name: "consecutive uppercase letters",
property: "HTTPSEndpoint",
kind: "http",
expected: `invalid retriever: no "httpsendpoint" property found for kind "http"`,
},
{
name: "empty property and kind",
property: "",
kind: "",
expected: `invalid retriever: no "" property found for kind ""`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewRetrieverConfError(tt.property, tt.kind)
got := err.CliErrorMessage()
assert.Equal(t, tt.expected, got, "CliErrorMessage() should convert camelCase to kebab-case")
})
}
}

func TestRetrieverConfError_ImplementsError(t *testing.T) {
var _ error = &RetrieverConfError{}
var _ error = NewRetrieverConfError("test", "test")
}
63 changes: 20 additions & 43 deletions cmdhelpers/retrieverconf/retriever_conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/redis/go-redis/v9"
"github.com/thomaspoignant/go-feature-flag/cmdhelpers/err"
)

var DefaultRetrieverConfig = struct {
Expand Down Expand Up @@ -74,19 +75,19 @@ func (c *RetrieverConf) IsValid() error {
return c.validateGitRetriever()
}
if c.Kind == S3Retriever && c.Item == "" {
return fmt.Errorf("invalid retriever: no \"item\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("item", string(c.Kind))
}
if c.Kind == HTTPRetriever && c.URL == "" {
return fmt.Errorf("invalid retriever: no \"url\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("url", string(c.Kind))
}
if c.Kind == GoogleStorageRetriever && c.Object == "" {
return fmt.Errorf("invalid retriever: no \"object\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("object", string(c.Kind))
}
if c.Kind == FileRetriever && c.Path == "" {
return fmt.Errorf("invalid retriever: no \"path\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("path", string(c.Kind))
}
if (c.Kind == S3Retriever || c.Kind == GoogleStorageRetriever) && c.Bucket == "" {
return fmt.Errorf("invalid retriever: no \"bucket\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("bucket", string(c.Kind))
}
if c.Kind == KubernetesRetriever {
return c.validateKubernetesRetriever()
Expand All @@ -106,90 +107,66 @@ func (c *RetrieverConf) IsValid() error {
// validatePostgreSQLRetriever validates the configuration of the postgresql retriever
func (c *RetrieverConf) validatePostgreSQLRetriever() error {
if c.URI == "" {
return fmt.Errorf("invalid retriever: no \"uri\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("uri", string(c.Kind))
}
if c.Table == "" {
return fmt.Errorf("invalid retriever: no \"table\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("table", string(c.Kind))
}
return nil
}

func (c *RetrieverConf) validateGitRetriever() error {
if c.RepositorySlug == "" {
return fmt.Errorf(
"invalid retriever: no \"repositorySlug\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("repositorySlug", string(c.Kind))
}
if c.Path == "" {
return fmt.Errorf("invalid retriever: no \"path\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("path", string(c.Kind))
}
return nil
}

func (c *RetrieverConf) validateKubernetesRetriever() error {
if c.ConfigMap == "" {
return fmt.Errorf(
"invalid retriever: no \"configmap\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("configmap", string(c.Kind))
}
if c.Namespace == "" {
return fmt.Errorf(
"invalid retriever: no \"namespace\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("namespace", string(c.Kind))
}
if c.Key == "" {
return fmt.Errorf("invalid retriever: no \"key\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("key", string(c.Kind))
}
return nil
}

func (c *RetrieverConf) validateMongoDBRetriever() error {
if c.Collection == "" {
return fmt.Errorf(
"invalid retriever: no \"collection\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("collection", string(c.Kind))
}
if c.Database == "" {
return fmt.Errorf(
"invalid retriever: no \"database\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("database", string(c.Kind))
}
if c.URI == "" {
return fmt.Errorf("invalid retriever: no \"uri\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("uri", string(c.Kind))
}
return nil
}

func (c *RetrieverConf) validateRedisRetriever() error {
if c.RedisOptions == nil {
return fmt.Errorf(
"invalid retriever: no \"redisOptions\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("redisOptions", string(c.Kind))
}
return nil
}

func (c *RetrieverConf) validateAzBlobStorageRetriever() error {
if c.AccountName == "" {
return fmt.Errorf(
"invalid retriever: no \"accountName\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("accountName", string(c.Kind))
}
if c.Container == "" {
return fmt.Errorf(
"invalid retriever: no \"container\" property found for kind \"%s\"",
c.Kind,
)
return err.NewRetrieverConfError("container", string(c.Kind))
}
if c.Object == "" {
return fmt.Errorf("invalid retriever: no \"object\" property found for kind \"%s\"", c.Kind)
return err.NewRetrieverConfError("object", string(c.Kind))
}
return nil
}
Expand Down
Loading