Skip to content
Open
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
44 changes: 44 additions & 0 deletions docs/api_docs/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,50 @@ paths:
description: Export JSON format of the eval cache dump
produces:
- application/json
parameters:
- in: query
name: ids
description: >-
"query optimized" flagIDs parameter. Has precedence over `enabled`,
`keys` and `tags` parameters.
required: false
type: array
collectionFormat: csv
minItems: 1
items:
type: integer
format: int64
minimum: 1
- in: query
name: keys
description: >-
"query optimized" flagKeys parameter. Has precedence over `enabled`
and `tags` parameter.
required: false
type: array
items:
type: string
minLength: 1
- in: query
name: enabled
description: return flags having given enabled status
required: false
type: boolean
- in: query
name: tags
description: '"query optimized" flagTags parameter'
required: false
type: array
items:
type: string
minLength: 1
- in: query
name: all
type: boolean
description: >-
whether to use ALL (tags) semantics (ANY by default):
`?tags=foo,bar&all=true` is equivalent to postEvaluation's
`flagTagsOperator: "ALL"`
responses:
'200':
description: OK
Expand Down
14 changes: 14 additions & 0 deletions pkg/entity/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ func GenFixtureFlag() Flag {
return f
}

func GenFixtureFlagWithTags(id uint, key string, enabled bool, tags []string) Flag {
f := Flag{
Model: gorm.Model{ID: id},
Key: key,
Description: "",
Enabled: enabled,
Tags: []Tag{},
}
for _, tag := range tags {
f.Tags = append(f.Tags, Tag{Value: tag})
}
return f
}

// GenFixtureSegment is a fixture
func GenFixtureSegment() Segment {
s := Segment{
Expand Down
72 changes: 69 additions & 3 deletions pkg/handler/eval_cache_fetcher.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package handler

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"

"encoding/json"
"slices"

"github.com/openflagr/flagr/pkg/config"
"github.com/openflagr/flagr/pkg/entity"
"github.com/openflagr/flagr/pkg/util"
"github.com/openflagr/flagr/swagger_gen/restapi/operations/export"
"gorm.io/gorm"
)

Expand All @@ -19,13 +20,78 @@ type EvalCacheJSON struct {
Flags []entity.Flag
}

func (ec *EvalCache) export() EvalCacheJSON {
func (ec *EvalCache) export(query export.GetExportEvalCacheJSONParams) EvalCacheJSON {
var targetIDs map[uint]struct{}
if len(query.Ids) > 0 {
targetIDs = make(map[uint]struct{}, len(query.Ids))
for _, id := range query.Ids {
targetIDs[uint(id)] = struct{}{}
}
}

var targetKeys map[string]struct{}
if len(query.Keys) > 0 {
targetKeys = make(map[string]struct{}, len(query.Keys))
for _, key := range query.Keys {
targetKeys[key] = struct{}{}
}
}

var hasTags func(*entity.Flag) bool
if len(query.Tags) > 0 {
if query.All != nil && *query.All {
hasTags = func(f *entity.Flag) bool {
for _, tag := range query.Tags {
if !slices.ContainsFunc(f.Tags, func(t entity.Tag) bool { return t.Value == tag }) {
return false
}
}
return true
}
} else {
hasTags = func(f *entity.Flag) bool {
for _, tag := range query.Tags {
if slices.ContainsFunc(f.Tags, func(t entity.Tag) bool { return t.Value == tag }) {
return true
}
}
return false
}
}
}

ec.cacheMutex.RLock()
defer ec.cacheMutex.RUnlock()

idCache := ec.cache.idCache
fs := make([]entity.Flag, 0, len(idCache))
for _, f := range idCache {
if targetIDs != nil {
if _, ok := targetIDs[f.ID]; ok {
ff := *f
fs = append(fs, ff)
}
continue
}

if targetKeys != nil {
if _, ok := targetKeys[f.Key]; ok {
ff := *f
fs = append(fs, ff)
}
continue
}

if query.Enabled != nil && *query.Enabled != f.Enabled {
continue
}

if hasTags != nil {
if !hasTags(f) {
continue
}
}

ff := *f
fs = append(fs, ff)
}
Expand Down
110 changes: 109 additions & 1 deletion pkg/handler/eval_cache_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package handler

import (
"github.com/openflagr/flagr/swagger_gen/models"
"slices"
"testing"

"github.com/openflagr/flagr/swagger_gen/models"
"github.com/openflagr/flagr/swagger_gen/restapi/operations/export"

"github.com/openflagr/flagr/pkg/entity"

"github.com/prashantv/gostub"
Expand Down Expand Up @@ -71,3 +74,108 @@ func TestGetByTags(t *testing.T) {
f = ec.GetByTags(tags, &all)
assert.Len(t, f, 0)
}

func TestEvalCacheExport(t *testing.T) {
ec := GenFixtureEvalCacheWithFlags([]entity.Flag{
entity.GenFixtureFlagWithTags(1, "first", true, []string{"tag1", "tag2"}),
entity.GenFixtureFlagWithTags(2, "second", true, []string{"tag2", "tag3"}),
entity.GenFixtureFlagWithTags(3, "third", false, []string{"tag2", "tag3"}),
entity.GenFixtureFlagWithTags(4, "fourth", true, []string{}),
})

t.Run("should be able to query cache via flag ids", func(t *testing.T) {
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Ids: []int64{1, 3}}).Flags
assert.Len(t, exportedFlags, 2)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(3)))
})

t.Run("should be able to query cache via flag keys", func(t *testing.T) {
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Keys: []string{"second", "fourth"}}).Flags
assert.Len(t, exportedFlags, 2)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(2)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))
})

t.Run("should be able to query cache via enabled property", func(t *testing.T) {
tru := true
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Enabled: &tru}).Flags
assert.Len(t, exportedFlags, 3)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(2)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))

fals := false
exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{Enabled: &fals}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(3)))
})

t.Run("should be able to query cache via tags with default ANY semantics", func(t *testing.T) {
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Tags: []string{"tag1", "tag2"}}).Flags
assert.Len(t, exportedFlags, 3)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(2)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(3)))

fals := false
exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{All: &fals, Tags: []string{"tag1", "tag2"}}).Flags
assert.Len(t, exportedFlags, 3)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(2)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(3)))
})

t.Run("should be able to query cache via tags with ALL semantics", func(t *testing.T) {
tru := true
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{All: &tru, Tags: []string{"tag1", "tag2"}}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
})

t.Run("flag ids query should have precedence over other queries", func(t *testing.T) {
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Ids: []int64{4}, Keys: []string{"first", "second"}}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))

fals := false
exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{Ids: []int64{4}, Enabled: &fals}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))

exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{Ids: []int64{4}, Tags: []string{"tag1"}}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))
})

t.Run("flag keys query should have precedence over enabled and tags queries", func(t *testing.T) {
fals := false
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Keys: []string{"fourth"}, Enabled: &fals}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))

exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{Keys: []string{"fourth"}, Tags: []string{"tag1"}}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(4)))
})

t.Run("should be able to combine enabled and tags queries", func(t *testing.T) {
tru := true
exportedFlags := ec.export(export.GetExportEvalCacheJSONParams{Enabled: &tru, Tags: []string{"tag2"}}).Flags
assert.Len(t, exportedFlags, 2)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(1)))
assert.True(t, slices.ContainsFunc(exportedFlags, withID(2)))

fals := false
exportedFlags = ec.export(export.GetExportEvalCacheJSONParams{Enabled: &fals, Tags: []string{"tag2"}}).Flags
assert.Len(t, exportedFlags, 1)
assert.True(t, slices.ContainsFunc(exportedFlags, withID(3)))
})
}

func withID(id uint) func(entity.Flag) bool {
return func(f entity.Flag) bool {
return f.ID == id
}
}
4 changes: 2 additions & 2 deletions pkg/handler/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ var exportFlagEntityTypes = func(tmpDB *gorm.DB) error {
return nil
}

var exportEvalCacheJSONHandler = func(export.GetExportEvalCacheJSONParams) middleware.Responder {
var exportEvalCacheJSONHandler = func(query export.GetExportEvalCacheJSONParams) middleware.Responder {
return export.NewGetExportEvalCacheJSONOK().WithPayload(
GetEvalCache().export(),
GetEvalCache().export(query),
)
}
26 changes: 26 additions & 0 deletions pkg/handler/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,29 @@ func GenFixtureEvalCache() *EvalCache {

return ec
}

func GenFixtureEvalCacheWithFlags(flags []entity.Flag) *EvalCache {
idCache := make(map[string]*entity.Flag)
keyCache := make(map[string]*entity.Flag)
tagCache := make(map[string]map[uint]*entity.Flag)
for _, f := range flags {
idCache[util.SafeString(f.Model.ID)] = &f
keyCache[f.Key] = &f
for _, tag := range f.Tags {
if tagCache[tag.Value] == nil {
tagCache[tag.Value] = make(map[uint]*entity.Flag)
}
tagCache[tag.Value][f.ID] = &f
}
}

ec := &EvalCache{
cache: &cacheContainer{
idCache: idCache,
keyCache: keyCache,
tagCache: tagCache,
},
}

return ec
}
46 changes: 46 additions & 0 deletions swagger/export_eval_cache_json.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ get:
description: Export JSON format of the eval cache dump
produces:
- application/json
parameters:
- in: query
name: ids
description: >-
"query optimized" flagIDs parameter.
Has precedence over `enabled`, `keys` and `tags` parameters.
required: false
type: array
collectionFormat: csv
minItems: 1
items:
type: integer
format: int64
minimum: 1
- in: query
name: keys
description: >-
"query optimized" flagKeys parameter.
Has precedence over `enabled` and `tags` parameter.
required: false
type: array
items:
type: string
minLength: 1
- in: query
name: enabled
description: >-
return flags having given enabled status
required: false
type: boolean
- in: query
name: tags
description: >-
"query optimized" flagTags parameter
required: false
type: array
items:
type: string
minLength: 1
- in: query
name: all
type: boolean
description: >-
whether to use ALL (tags) semantics (ANY by default):
`?tags=foo,bar&all=true` is equivalent to postEvaluation's
`flagTagsOperator: "ALL"`
responses:
200:
description: OK
Expand Down
Loading
Loading