Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a67d321
fix: main-mappers-and-tests
Zaimwa9 Oct 23, 2025
dc19da6
fix: added-metadata
Zaimwa9 Oct 23, 2025
d536c81
fix: renaming
Zaimwa9 Oct 23, 2025
79b3176
fix: comments
Zaimwa9 Oct 23, 2025
45b926e
fix: remoevd-redundant-test
Zaimwa9 Oct 23, 2025
9ea952d
feat: reviewed-engine-test-todos
Zaimwa9 Oct 23, 2025
b9d14a8
Update lib/flagsmith/engine/evaluation/mappers.rb
Zaimwa9 Oct 24, 2025
cb3558b
feat: use-overrides-key-hash
Zaimwa9 Oct 24, 2025
58bfe8b
feat: removed-identifiers-join
Zaimwa9 Oct 24, 2025
3d7fa00
feat: flagsmith-id-in-snake-case
Zaimwa9 Oct 24, 2025
075a8a4
feat: run-lint
Zaimwa9 Oct 28, 2025
73b841c
feat: renamed-module-evaluation
Zaimwa9 Oct 28, 2025
51894ad
feat: fixed-priority-0-being-skipped
Zaimwa9 Oct 28, 2025
e1eabd2
feat: reverted-to-hash
Zaimwa9 Oct 30, 2025
0386836
feat: linter
Zaimwa9 Nov 10, 2025
d86ba90
feat: split-functions-and-module-for-linting
Zaimwa9 Nov 10, 2025
f8ea862
feat: get-rid-of-extra-map-nested-rule-function
Zaimwa9 Nov 10, 2025
b2c3d30
feat: renaming-identity-methods
Zaimwa9 Nov 10, 2025
7de46c2
feat: fixed-forgotten-func-renaming
Zaimwa9 Nov 10, 2025
10b256c
Update lib/flagsmith/engine/evaluation/mappers/environment.rb
Zaimwa9 Nov 10, 2025
6c8dc0b
feat: added-name-env-model-and-fixture
Zaimwa9 Nov 10, 2025
1a908fe
feat: moved-mappers-to-engine-namespace
Zaimwa9 Nov 10, 2025
0c9650a
feat: removed-feature-key
Zaimwa9 Nov 11, 2025
4b106a7
feat: linter
Zaimwa9 Nov 11, 2025
f1026a2
feat: get evaluation get result (#88)
Zaimwa9 Nov 12, 2025
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
185 changes: 15 additions & 170 deletions lib/flagsmith/engine/evaluation/mappers.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# frozen_string_literal: true

require_relative 'mappers/environment'
require_relative 'mappers/identity'
require_relative 'mappers/segments'

module Flagsmith
module Engine
module Evaluation
Expand All @@ -13,12 +17,8 @@ module Mappers
# @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
# @return [Hash] Evaluation context with environment, features, segments, and optionally identity
def self.get_evaluation_context(environment, identity = nil, override_traits = nil)
environment_context = map_environment_model_to_evaluation_context(environment)
identity_context = identity ? map_identity_model_to_identity_context(identity, override_traits) : nil

context = environment_context.dup
context[:identity] = identity_context if identity_context

context = map_environment_model_to_evaluation_context(environment)
context[:identity] = map_identity_model_to_identity_context(identity, override_traits) if identity
context
end

Expand All @@ -27,85 +27,15 @@ def self.get_evaluation_context(environment, identity = nil, override_traits = n
# @param environment [Flagsmith::Engine::Environment] The environment model
# @return [Hash] Context with :environment, :features, and :segments keys
def self.map_environment_model_to_evaluation_context(environment)
environment_context = {
key: environment.api_key,
name: environment.project.name
context = {
environment: Environment.build_environment_context(environment),
features: Environment.build_features_context(environment.feature_states),
segments: Segments.build_segments_context(environment.project.segments)
}

# Map feature states to features hash
features = {}
environment.feature_states.each do |fs|
# Map multivariate values if present
variants = nil
if fs.multivariate_feature_state_values&.any?
variants = fs.multivariate_feature_state_values.map do |mv|
{
value: mv.multivariate_feature_option.value,
weight: mv.percentage_allocation,
priority: mv.id || uuid_to_big_int(mv.mv_fs_value_uuid)
}
end
end

feature_hash = {
key: fs.django_id&.to_s || fs.uuid,
feature_key: fs.feature.id.to_s,
name: fs.feature.name,
enabled: fs.enabled,
value: fs.get_value
}

feature_hash[:variants] = variants if variants
priority = fs.feature_segment&.priority
feature_hash[:priority] = priority unless priority.nil?
feature_hash[:metadata] = { flagsmith_id: fs.feature.id }

features[fs.feature.name] = feature_hash
end

# Map segments from project
segments = {}
environment.project.segments.each do |segment|
overrides = segment.feature_states.map do |fs|
override_hash = {
key: fs.django_id&.to_s || fs.uuid,
feature_key: fs.feature.id.to_s,
name: fs.feature.name,
enabled: fs.enabled,
value: fs.get_value
}
override_hash[:priority] = fs.feature_segment.priority if fs.feature_segment&.priority
override_hash[:metadata] = { flagsmith_id: fs.feature.id }
override_hash
end
context[:segments].merge!(Identity.map_overrides_to_segments(environment.identity_overrides)) if environment.identity_overrides&.any?

segments[segment.id.to_s] = {
key: segment.id.to_s,
name: segment.name,
rules: segment.rules.map { |rule| map_segment_rule_model_to_rule(rule) },
overrides: overrides,
metadata: {
source: 'API',
flagsmith_id: segment.id
}
}
end

# Map identity overrides to segments
if environment.identity_overrides&.any?
identity_override_segments = map_identity_overrides_to_segments(environment.identity_overrides)
segments.merge!(identity_override_segments)
end

{
environment: environment_context,
features: features,
segments: segments
}
end

def self.uuid_to_big_int(uuid)
uuid.gsub('-', '').to_i(16)
context
end

# Maps identity model to identity context
Expand All @@ -114,108 +44,23 @@ def self.uuid_to_big_int(uuid)
# @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
# @return [Hash] Identity context with :identifier, :key, and :traits
def self.map_identity_model_to_identity_context(identity, override_traits = nil)
# Use override traits if provided, otherwise use identity's traits
traits = override_traits || identity.identity_traits

# Map traits to a hash with trait key => trait value
traits_hash = {}
traits.each do |trait|
traits_hash[trait.trait_key] = trait.trait_value
end

{
identifier: identity.identifier,
key: identity.django_id&.to_s || identity.composite_key,
traits: traits_hash
}
Identity.build_environment_context(identity, override_traits)
end

# Maps segment rule model to rule hash
#
# @param rule [Flagsmith::Engine::Segments::Rule] The segment rule model
# @return [Hash] Mapped rule with :type, :conditions, and :rules
def self.map_segment_rule_model_to_rule(rule)
result = {
type: rule.type
}

# Map conditions if present
result[:conditions] = (rule.conditions || []).map do |condition|
{ property: condition.property, operator: condition.operator, value: condition.value }
end

result[:rules] = if rule.rules&.any?
rule.rules.map { |nested_rule| map_segment_rule_model_to_rule(nested_rule) }
else
[]
end

result
Segments.map_rule(rule)
end

# Maps identity overrides to segments
#
# @param identity_overrides [Array<Flagsmith::Engine::Identity>] Array of identity override models
# @return [Hash] Segments hash for identity overrides
def self.map_identity_overrides_to_segments(identity_overrides)
segments = {}
features_to_identifiers = {}

identity_overrides.each do |identity|
next if identity.identity_features.nil? || identity.identity_features.none?

# Sort features by name for consistent hashing
sorted_features = identity.identity_features.to_a.sort_by { |fs| fs.feature.name }

# Create override keys for hashing
overrides_key = sorted_features.map do |fs|
{
feature_key: fs.feature.id.to_s,
name: fs.feature.name,
enabled: fs.enabled,
value: fs.get_value,
priority: STRONGEST_PRIORITY,
metadata: {
flagsmith_id: fs.feature.id
}
}
end

# Create hash of the overrides to group identities with same overrides
overrides_hash = overrides_key.hash

features_to_identifiers[overrides_hash] ||= { identifiers: [], overrides: overrides_key }
features_to_identifiers[overrides_hash][:identifiers] << identity.identifier
end

# Create segments for each unique set of overrides
features_to_identifiers.each do |overrides_hash, data|
segment_key = "identity_override_#{overrides_hash}"

segments[segment_key] = {
key: segment_key,
name: 'identity_override',
rules: [
{
type: 'ALL',
conditions: [
{
property: '$.identity.identifier',
operator: 'IN',
value: data[:identifiers]
}
],
rules: []
}
],
metadata: {
source: 'identity_override'
},
overrides: data[:overrides]
}
end

segments
Identity.map_overrides_to_segments(identity_overrides)
end
end
end
Expand Down
62 changes: 62 additions & 0 deletions lib/flagsmith/engine/evaluation/mappers/environment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Flagsmith
module Engine
module Evaluation
module Mappers
# Handles environment and feature mapping
module Environment
def self.build_environment_context(environment)
{
key: environment.api_key,
name: environment.project.name
}
end

def self.build_features_context(feature_states)
features = {}
feature_states.each do |feature_state|
features[feature_state.feature.name] = build_feature_hash(feature_state)
end
features
end

def self.build_feature_hash(feature_state) # rubocop:disable Metrics/MethodLength
feature_hash = {
key: feature_state.django_id&.to_s || feature_state.uuid,
feature_key: feature_state.feature.id.to_s,
name: feature_state.feature.name,
enabled: feature_state.enabled,
value: feature_state.get_value,
metadata: { flagsmith_id: feature_state.feature.id }
}
add_variants_to_feature(feature_hash, feature_state)
add_priority_to_feature(feature_hash, feature_state)
feature_hash
end

def self.add_variants_to_feature(feature_hash, feature_state)
return unless feature_state.multivariate_feature_state_values&.any?

feature_hash[:variants] = feature_state.multivariate_feature_state_values.map do |mv|
{
value: mv.multivariate_feature_option.value,
weight: mv.percentage_allocation,
priority: mv.id || uuid_to_big_int(mv.mv_fs_value_uuid)
}
end
end

def self.uuid_to_big_int(uuid)
uuid.gsub('-', '').to_i(16)
end

def self.add_priority_to_feature(feature_hash, feature_state)
priority = feature_state.feature_segment&.priority
feature_hash[:priority] = priority unless priority.nil?
end
end
end
end
end
end
100 changes: 100 additions & 0 deletions lib/flagsmith/engine/evaluation/mappers/identity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

module Flagsmith
module Engine
module Evaluation
module Mappers
# Handles identity and override mapping
module Identity
def self.build_environment_context(identity, override_traits = nil)
traits = override_traits || identity.identity_traits

{
identifier: identity.identifier,
key: identity.django_id&.to_s || identity.composite_key,
traits: build_traits_hash(traits)
}
end

def self.build_traits_hash(traits)
traits_hash = {}
traits.each do |trait|
traits_hash[trait.trait_key] = trait.trait_value
end
traits_hash
end

def self.map_overrides_to_segments(identity_overrides)
features_to_identifiers = group_by_overrides(identity_overrides)
build_segments(features_to_identifiers)
end

def self.group_by_overrides(identity_overrides)
features_to_identifiers = {}

identity_overrides.each do |identity|
next if identity.identity_features.nil? || identity.identity_features.none?

overrides_key = build_overrides_key(identity.identity_features)
overrides_hash = overrides_key.hash

features_to_identifiers[overrides_hash] ||= { identifiers: [], overrides: overrides_key }
features_to_identifiers[overrides_hash][:identifiers] << identity.identifier
end

features_to_identifiers
end

def self.build_overrides_key(identity_features) # rubocop:disable Metrics/MethodLength
sorted_features = identity_features.to_a.sort_by { |fs| fs.feature.name }
sorted_features.map do |feature_state|
{
feature_key: feature_state.feature.id.to_s,
name: feature_state.feature.name,
enabled: feature_state.enabled,
value: feature_state.get_value,
priority: Mappers::STRONGEST_PRIORITY,
metadata: { flagsmith_id: feature_state.feature.id }
}
end
end

def self.build_segments(features_to_identifiers)
segments = {}

features_to_identifiers.each do |overrides_hash, data|
segment_key = "identity_override_#{overrides_hash}"
segments[segment_key] = build_segment(segment_key, data)
end

segments
end

def self.build_segment(segment_key, data)
{
key: segment_key,
name: 'identity_override',
rules: [build_rule(data[:identifiers])],
metadata: { source: 'identity_override' },
overrides: data[:overrides]
}
end

def self.build_rule(identifiers) # rubocop:disable Metrics/MethodLength
{
type: 'ALL',
conditions: [
{
property: '$.identity.identifier',
operator: 'IN',
value: identifiers
}
],
rules: []
}
end
end
end
end
end
end
Loading