diff --git a/apis/flags.go b/apis/flags.go index ad48b8b..33f08bf 100644 --- a/apis/flags.go +++ b/apis/flags.go @@ -5,19 +5,19 @@ import ( "net/http" custom_errors "github.com/cheetahbyte/flagly/internal/error" - "github.com/cheetahbyte/flagly/internal/evaluation" "github.com/cheetahbyte/flagly/internal/utils" "github.com/cheetahbyte/flagly/pkg/flagly" "github.com/gin-gonic/gin" ) type FlagAPI struct { - store *flagly.Storage - auditService flagly.AuditService + store *flagly.Storage + auditService flagly.AuditService + evaluationService flagly.EvaluationService } -func NewFlagAPI(store *flagly.Storage, auditService flagly.AuditService) *FlagAPI { - return &FlagAPI{store: store, auditService: auditService} +func NewFlagAPI(store *flagly.Storage, auditService flagly.AuditService, evaluationService flagly.EvaluationService) *FlagAPI { + return &FlagAPI{store: store, auditService: auditService, evaluationService: evaluationService} } func (api *FlagAPI) RegisterRoutes(router *gin.RouterGroup) { @@ -101,7 +101,7 @@ func (api *FlagAPI) PostEvaluateFlag(c *gin.Context) { return } - result := evaluation.EvaluateFlag(*flag, data.User, data.Environment) + result, _ := api.evaluationService.EvaluateFlag(*flag, data.User, data.Environment) api.auditService.TrackEvaluation(c, *flag, data.User, data.Environment, result) diff --git a/internal/audit/audit.go b/internal/audit/audit.go index 3691955..aede87b 100644 --- a/internal/audit/audit.go +++ b/internal/audit/audit.go @@ -6,13 +6,9 @@ import ( "github.com/gin-gonic/gin" ) -type AuditService interface { - TrackEvaluation(c *gin.Context, flag flagly.Flag, user flagly.User, environment string, result bool) -} - type DefaultAuditService struct{} -func NewDefaultAuditService() AuditService { +func NewDefaultAuditService() flagly.AuditService { return &DefaultAuditService{} } diff --git a/internal/evaluation/evaluate.go b/internal/evaluation/evaluate.go index 2cb79d1..5a060f4 100644 --- a/internal/evaluation/evaluate.go +++ b/internal/evaluation/evaluate.go @@ -7,21 +7,25 @@ import ( "github.com/spaolacci/murmur3" ) -// calculates the rollout percentage bucket in which the user is located. +type DefaultEvaluationService struct{} + +func NewDefaultAuditService() flagly.EvaluationService { + return &DefaultEvaluationService{} +} + func calculateRolloutBucket(identifier string) int { hash := murmur3.Sum32([]byte(identifier)) return int(hash % 100) } -func EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) bool { - // check if environment is enabled +func (s *DefaultEvaluationService) EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) (bool, error) { env, ok := flag.Environments[environment] if !ok { - return false + return false, nil } fmt.Println(env) if env.Rollout.Percentage == 0 || env.Rollout.Percentage == 100 { - return true + return true, nil } // TODO: make this more dynamic @@ -35,5 +39,5 @@ func EvaluateFlag(flag flagly.Flag, user flagly.User, environment string) bool { // Rollout specific logic hashedPercentage := calculateRolloutBucket(stickiness) - return env.Rollout.Percentage > hashedPercentage + return env.Rollout.Percentage > hashedPercentage, nil } diff --git a/internal/evaluation/evaluate_test.go b/internal/evaluation/evaluate_test.go index 48b2040..991f168 100644 --- a/internal/evaluation/evaluate_test.go +++ b/internal/evaluation/evaluate_test.go @@ -8,18 +8,23 @@ import ( ) func TestEvaluateFlag_UnknownEnvironment(t *testing.T) { + service := &DefaultEvaluationService{} flag := flagly.Flag{ Environments: map[string]flagly.Environment{}, } user := flagly.User{ID: "123"} - result := EvaluateFlag(flag, user, "production") + result, err := service.EvaluateFlag(flag, user, "production") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } if result { t.Errorf("expected false for unknown environment, got true") } } func TestEvaluateFlag_PercentageZero(t *testing.T) { + service := &DefaultEvaluationService{} flag := flagly.Flag{ Environments: map[string]flagly.Environment{ "production": { @@ -30,12 +35,17 @@ func TestEvaluateFlag_PercentageZero(t *testing.T) { } user := flagly.User{ID: "user1"} - if !EvaluateFlag(flag, user, "production") { - t.Errorf("expected true when rollout is 0%% or 100%%, got false") + result, err := service.EvaluateFlag(flag, user, "production") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result { + t.Errorf("expected true when rollout is 0%%, got false") } } func TestEvaluateFlag_Percentage100(t *testing.T) { + service := &DefaultEvaluationService{} flag := flagly.Flag{ Environments: map[string]flagly.Environment{ "production": { @@ -46,12 +56,17 @@ func TestEvaluateFlag_Percentage100(t *testing.T) { } user := flagly.User{ID: "user1"} - if !EvaluateFlag(flag, user, "production") { - t.Errorf("expected true for 100%% rollout") + result, err := service.EvaluateFlag(flag, user, "production") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result { + t.Errorf("expected true for 100%% rollout, got false") } } func TestEvaluateFlag_ConsistentRollout(t *testing.T) { + service := &DefaultEvaluationService{} user := flagly.User{ID: "user42"} flag := flagly.Flag{ Environments: map[string]flagly.Environment{ @@ -62,17 +77,21 @@ func TestEvaluateFlag_ConsistentRollout(t *testing.T) { }, } - // Should always return the same result for the same user - result1 := EvaluateFlag(flag, user, "production") - result2 := EvaluateFlag(flag, user, "production") + result1, err1 := service.EvaluateFlag(flag, user, "production") + result2, err2 := service.EvaluateFlag(flag, user, "production") + if err1 != nil || err2 != nil { + t.Fatalf("unexpected error(s): %v, %v", err1, err2) + } if result1 != result2 { t.Errorf("expected consistent result for same user, got %v and %v", result1, result2) } } func TestEvaluateFlag_WithinRollout(t *testing.T) { - // Find a user ID that gives a bucket under 10 + service := &DefaultEvaluationService{} + + // Find a user ID that hashes to a bucket < 10 var userID string for i := 0; i < 10000; i++ { id := "user" + strconv.Itoa(i) @@ -81,7 +100,6 @@ func TestEvaluateFlag_WithinRollout(t *testing.T) { break } } - if userID == "" { t.Fatalf("could not find user ID with bucket < 10") } @@ -96,7 +114,11 @@ func TestEvaluateFlag_WithinRollout(t *testing.T) { }, } - if !EvaluateFlag(flag, user, "production") { + result, err := service.EvaluateFlag(flag, user, "production") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result { t.Errorf("expected true for user within rollout range") } } diff --git a/main.go b/main.go index 2c409b1..dbd57b9 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/cheetahbyte/flagly/apis" "github.com/cheetahbyte/flagly/internal/audit" + "github.com/cheetahbyte/flagly/internal/evaluation" "github.com/cheetahbyte/flagly/internal/storage" "github.com/cheetahbyte/flagly/pkg/flagly/middleware" "github.com/gin-contrib/cors" @@ -45,10 +46,11 @@ func main() { router.Use(middleware.ErrorHandlerMiddleware()) auditService := audit.NewDefaultAuditService() + evaluationService := evaluation.NewDefaultAuditService() apiGroup := router.Group("/api") apis.NewGeneralAPI(store).RegisterRoutes(apiGroup) - apis.NewFlagAPI(store, auditService).RegisterRoutes(apiGroup) + apis.NewFlagAPI(store, auditService, evaluationService).RegisterRoutes(apiGroup) apis.NewEnvironmentAPI(store).RegisterRoutes(apiGroup) if os.Getenv("GIN_MODE") == "release" { diff --git a/pkg/flagly/evaluation.go b/pkg/flagly/evaluation.go new file mode 100644 index 0000000..7092ea3 --- /dev/null +++ b/pkg/flagly/evaluation.go @@ -0,0 +1,5 @@ +package flagly + +type EvaluationService interface { + EvaluateFlag(flag Flag, user User, environment string) (bool, error) +}