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
186 changes: 186 additions & 0 deletions dtos/configmapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package dtos

import "encoding/json"

// ConvertConfigToSplit converts a ConfigDTO to a SplitDTO
// This mapper bridges the /configs endpoint response to the internal Split representation
// used by the evaluator.
func ConvertConfigToSplit(config ConfigDTO) SplitDTO {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

you need to extract variants element as well, see comment below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated

// Apply defaults as per SDK spec
trafficTypeName := config.TrafficTypeName
if trafficTypeName == "" {
trafficTypeName = "user"
}

// Default to ACTIVE if status is missing
status := config.Status
if status == "" {
status = "ACTIVE"
}

// Map Targeting.Default to DefaultTreatment
defaultTreatment := config.Targeting.Default
if defaultTreatment == "" {
defaultTreatment = "control"
}

// Build configurations map from Variants
// Each variant's Definition (acts as directive for evaluator) is marshaled to JSON
configurations := make(map[string]string)
for _, variant := range config.Variants {
if variant.Definition != nil {
// Marshal the Definition (directive) to JSON string
definitionJSON, err := json.Marshal(variant.Definition)
if err == nil {
configurations[variant.Name] = string(definitionJSON)
}
}
}

// Transform raw conditions from /configs format to evaluator ConditionDTO format
conditions := make([]ConditionDTO, 0, len(config.Targeting.Conditions)+1)
for _, rawCondition := range config.Targeting.Conditions {
conditions = append(conditions, convertRawCondition(rawCondition))
}

// ALWAYS append default rule condition at the end
conditions = append(conditions, ConditionDTO{
ConditionType: "ROLLOUT",
Label: "default rule",
MatcherGroup: MatcherGroupDTO{
Combiner: "AND",
Matchers: []MatcherDTO{
{
MatcherType: "ALL_KEYS",
Negate: false,
KeySelector: nil,
},
},
},
Partitions: []PartitionDTO{
{
Treatment: defaultTreatment,
Size: 100,
},
},
})

// Build the SplitDTO for the evaluator
return SplitDTO{
Name: config.Name,
Killed: config.Killed, // Defaults to false if missing in JSON
ChangeNumber: config.ChangeNumber,
Configurations: configurations,
DefaultTreatment: defaultTreatment,
TrafficTypeName: trafficTypeName,
Status: status,
Algo: 2, // Explicitly set to 2 (Murmur3) - not provided by /configs endpoint
Seed: config.Targeting.Seed,
TrafficAllocation: config.Targeting.TrafficAllocation,
TrafficAllocationSeed: config.Targeting.TrafficAllocationSeed,
Conditions: conditions,
Sets: config.Sets, // Feature flag sets
}
}

// convertRawCondition transforms a raw condition from /configs format to ConditionDTO
func convertRawCondition(raw RawConditionDTO) ConditionDTO {
// Determine condition type based on matchers
// If any matcher is WHITELIST type, the whole condition is WHITELIST, otherwise ROLLOUT
conditionType := "ROLLOUT"
for _, matcher := range raw.Matchers {
if matcher.Type == "WHITELIST" {
conditionType = "WHITELIST"
break
}
}

// Convert matchers
matchers := make([]MatcherDTO, 0, len(raw.Matchers))
for _, rawMatcher := range raw.Matchers {
matchers = append(matchers, convertMatcher(rawMatcher))
}

// Convert partitions: variant -> treatment
partitions := make([]PartitionDTO, 0, len(raw.Partitions))
for _, rawPartition := range raw.Partitions {
partitions = append(partitions, PartitionDTO{
Treatment: rawPartition.Variant, // Map "variant" to "treatment"
Size: rawPartition.Size,
})
}

return ConditionDTO{
ConditionType: conditionType,
Label: raw.Label,
MatcherGroup: MatcherGroupDTO{
Combiner: "AND", // Always AND for /configs conditions
Matchers: matchers,
},
Partitions: partitions,
}
}

// convertMatcher transforms a raw matcher from /configs format to MatcherDTO
func convertMatcher(raw RawMatcherDTO) MatcherDTO {
// Build keySelector if attribute is provided
var keySelector *KeySelectorDTO
if raw.Attribute != "" {
attr := raw.Attribute
keySelector = &KeySelectorDTO{
TrafficType: "user",
Attribute: &attr,
}
}

// Transform based on matcher type
switch raw.Type {
case "WHITELIST":
// Extract strings from data
var whitelist []string
if stringsData, ok := raw.Data["strings"].([]interface{}); ok {
whitelist = make([]string, 0, len(stringsData))
for _, s := range stringsData {
if str, ok := s.(string); ok {
whitelist = append(whitelist, str)
}
}
}
return MatcherDTO{
MatcherType: "WHITELIST",
Negate: false,
KeySelector: keySelector,
Whitelist: &WhitelistMatcherDataDTO{
Whitelist: whitelist,
},
}

case "IS_EQUAL_TO":
// Map to EQUAL_TO with unary numeric data
var dataType string
var value int64
if dt, ok := raw.Data["type"].(string); ok {
dataType = dt
}
if num, ok := raw.Data["number"].(float64); ok {
value = int64(num)
}
return MatcherDTO{
MatcherType: "EQUAL_TO",
Negate: false,
KeySelector: keySelector,
UnaryNumeric: &UnaryNumericMatcherDataDTO{
DataType: dataType,
Value: value,
},
}

default:
// Default to ALL_KEYS for unknown types
return MatcherDTO{
MatcherType: "ALL_KEYS",
Negate: false,
KeySelector: keySelector,
}
}
}
117 changes: 117 additions & 0 deletions dtos/configs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package dtos

import "encoding/json"

// VariantDTO represents a variant (treatment) with its definition (directive for evaluator)
type VariantDTO struct {
Name string `json:"name"`
Definition interface{} `json:"definition"` // Acts as directive for the evaluator
}

// RawMatcherDTO represents a matcher as returned by the /configs endpoint (needs transformation)
type RawMatcherDTO struct {
Type string `json:"type"` // e.g., "WHITELIST", "IS_EQUAL_TO", "GREATER_THAN_OR_EQUAL_TO"
Attribute string `json:"attribute"` // Attribute name (optional for ALL_KEYS)
Data map[string]interface{} `json:"data"` // Flexible data structure
}

// RawPartitionDTO represents a partition as returned by the /configs endpoint
type RawPartitionDTO struct {
Variant string `json:"variant"` // Uses "variant" instead of "treatment"
Size int `json:"size"`
}

// RawConditionDTO represents a condition as returned by the /configs endpoint (needs transformation)
type RawConditionDTO struct {
Label string `json:"label"`
Partitions []RawPartitionDTO `json:"partitions"`
Matchers []RawMatcherDTO `json:"matchers"` // Flat array, not MatcherGroup
// Note: No ConditionType field in raw conditions
}

// TargetingDTO represents the targeting rules for a config
type TargetingDTO struct {
Default string `json:"default"` // The default variant name
Seed int64 `json:"seed"` // Seed for hashing
TrafficAllocation int `json:"trafficAllocation"` // Percentage of traffic allocated
TrafficAllocationSeed int64 `json:"trafficAllocationSeed"` // Seed for traffic allocation
Conditions []RawConditionDTO `json:"conditions"` // Raw targeting conditions (need transformation)
}

// ConfigDTO represents a configuration definition fetched from the /configs endpoint
type ConfigDTO struct {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same here, missing variants and targeting elements

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated

Name string `json:"name"`
TrafficTypeName string `json:"trafficTypeName"`
ChangeNumber int64 `json:"changeNumber"`
Status string `json:"status"` // Defaults to "ACTIVE" if missing
Killed bool `json:"killed"` // Defaults to false if missing
Sets []string `json:"sets"` // Feature flag sets
Variants []VariantDTO `json:"variants"`
Targeting TargetingDTO `json:"targeting"`
}

// ConfigsResponseDTO represents the response from the /configs endpoint
type ConfigsResponseDTO struct {
Updated []ConfigDTO `json:"updated"` // List of updated configs
Since int64 `json:"since"` // Starting change number
Till int64 `json:"till"` // Ending change number
RBS []RuleBasedSegmentDTO `json:"rbs"` // Rule-based segments
}

// FFResponseConfigs implements FFResponse interface for configs endpoint responses
type FFResponseConfigs struct {
configsResponse ConfigsResponseDTO
}

// NewFFResponseConfigs creates a new FFResponseConfigs instance from JSON data
func NewFFResponseConfigs(data []byte) (FFResponse, error) {
var configsResponse ConfigsResponseDTO
err := json.Unmarshal(data, &configsResponse)
if err != nil {
return nil, err
}
return &FFResponseConfigs{
configsResponse: configsResponse,
}, nil
}

// NeedsAnotherFetch checks if another fetch is needed based on the since and till values
func (f *FFResponseConfigs) NeedsAnotherFetch() bool {
return f.configsResponse.Since == f.configsResponse.Till
}

// RuleBasedSegments returns the list of rule-based segments from the response
func (f *FFResponseConfigs) RuleBasedSegments() []RuleBasedSegmentDTO {
return f.configsResponse.RBS
}

// FeatureFlags returns the list of feature flags (splits) converted from configs
func (f *FFResponseConfigs) FeatureFlags() []SplitDTO {
splits := make([]SplitDTO, 0, len(f.configsResponse.Updated))
for _, config := range f.configsResponse.Updated {
splits = append(splits, ConvertConfigToSplit(config))
}
return splits
}

// FFTill returns the till value for feature flags
func (f *FFResponseConfigs) FFTill() int64 {
return f.configsResponse.Till
}

// RBTill returns the till value for rule-based segments
func (f *FFResponseConfigs) RBTill() int64 {
return f.configsResponse.Till
}

// FFSince returns the since value for feature flags
func (f *FFResponseConfigs) FFSince() int64 {
return f.configsResponse.Since
}

// RBSince returns the since value for rule-based segments
func (f *FFResponseConfigs) RBSince() int64 {
return f.configsResponse.Since
}

var _ FFResponse = (*FFResponseConfigs)(nil)
Loading
Loading