Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2aeb65a
feat: added-engine-function-signatures
Zaimwa9 Oct 24, 2025
ba057cb
feat: moved-engine-to-core
Zaimwa9 Oct 24, 2025
bb96eb1
feat: implemented-process-segment-overrides
Zaimwa9 Oct 24, 2025
a2fd5a8
feat: implemented-evalute-segments-partially
Zaimwa9 Oct 24, 2025
0f08fb1
feat: implemented-should-apply-override
Zaimwa9 Oct 24, 2025
da08e8e
feat: implemented-get-identity-segments
Zaimwa9 Oct 27, 2025
af51bf7
feat: implemented-new-in-and-fixed-remaining-tests
Zaimwa9 Oct 27, 2025
0fb7587
feat: run-lint
Zaimwa9 Oct 28, 2025
741ceeb
feat: rebased
Zaimwa9 Oct 28, 2025
6a6a129
feat: misc
Zaimwa9 Oct 28, 2025
3bad95b
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Oct 30, 2025
f0a53b7
feat: json-path-lib-implementation
Zaimwa9 Oct 30, 2025
ef1274a
remove dup
gagantrivedi Nov 5, 2025
962c01c
feat: made-legacy-functions-public
Zaimwa9 Nov 10, 2025
771a1a1
feat: updated-tests-to-match-engine-in-operator-accepting-numbers
Zaimwa9 Nov 10, 2025
561cd71
feat: rebased
Zaimwa9 Nov 10, 2025
21d7e74
feat: engine-agnostic-to-empty-identity-in-segment-evaluation
Zaimwa9 Nov 10, 2025
0baa0b4
feat: renamed-to-is-higher-priority
Zaimwa9 Nov 10, 2025
97cfdfb
feat: renamed-get-identity-segments-func
Zaimwa9 Nov 10, 2025
7188b95
feat: reverted-to-is-primitive
Zaimwa9 Nov 10, 2025
418ae33
feat: use-weakest-priority-constant
Zaimwa9 Nov 10, 2025
12144a3
feat: upgraded-engine-test-data-and-fixed-mv-evaluation-bug
Zaimwa9 Nov 10, 2025
fe63b45
feat: removed-targeting-reason-func
Zaimwa9 Nov 10, 2025
6420d15
feat: linter-rubocop-autocorrect
Zaimwa9 Nov 10, 2025
cbbd3c2
feat: linter
Zaimwa9 Nov 10, 2025
3b729fc
feat: linter
Zaimwa9 Nov 10, 2025
7c15dd5
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Nov 10, 2025
13057f9
feat: moved-mappers-to-engine-namespace
Zaimwa9 Nov 10, 2025
7cee757
feat: rebased
Zaimwa9 Nov 11, 2025
2f3850c
Merge branch 'feat/evaluation-context-mappers' of github.com:Flagsmit…
Zaimwa9 Nov 11, 2025
37c424f
feat: enrich-context-with-identity-key
Zaimwa9 Nov 11, 2025
29e6bf6
feat: run-ci-on-all-branches
Zaimwa9 Nov 11, 2025
080f191
feat: removed-comments
Zaimwa9 Nov 11, 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
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ PATH
flagsmith (4.3.0)
faraday (>= 2.0.1)
faraday-retry
jsonpath (~> 1.1)
semantic

GEM
Expand All @@ -21,8 +22,11 @@ GEM
faraday (~> 2.0)
gem-release (2.2.2)
json (2.7.1)
jsonpath (1.1.5)
multi_json
language_server-protocol (3.17.0.3)
method_source (1.0.0)
multi_json (1.17.0)
net-http (0.4.1)
uri
parallel (1.24.0)
Expand Down
1 change: 1 addition & 0 deletions flagsmith.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Gem::Specification.new do |spec|

spec.add_dependency 'faraday', '>= 2.0.1'
spec.add_dependency 'faraday-retry'
spec.add_dependency 'jsonpath', '~> 1.1'
spec.add_dependency 'semantic'
spec.metadata['rubygems_mfa_required'] = 'true'
end
1 change: 1 addition & 0 deletions lib/flagsmith/engine/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require_relative 'segments/models'
require_relative 'utils/hash_func'
require_relative 'evaluation/mappers'
require_relative 'evaluation/core'

module Flagsmith
module Engine
Expand Down
161 changes: 157 additions & 4 deletions lib/flagsmith/engine/evaluation/core.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,173 @@
# frozen_string_literal: true

require_relative '../utils/hash_func'
require_relative '../features/constants'
require_relative '../segments/evaluator'

module Flagsmith
module Engine
module Evaluation
# Core evaluation logic module
module Core
extend self
include Flagsmith::Engine::Utils::HashFunc
include Flagsmith::Engine::Features::TargetingReasons
include Flagsmith::Engine::Segments::Evaluator
# Get evaluation result from evaluation context
#
# @param evaluation_context [Hash] The evaluation context
# @return [Hash] Evaluation result with flags and segments
def self.get_evaluation_result(_evaluation_context)
# TODO: Implement core evaluation logic
# returns EvaluationResultWithMetadata
def get_evaluation_result(evaluation_context)
segments, segment_overrides = evaluate_segments(evaluation_context)
flags = evaluate_features(evaluation_context, segment_overrides)
{
flags: {},
segments: []
flags: flags,
segments: segments
}
end

# Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; }
def evaluate_segments(evaluation_context)
return [], {} if evaluation_context[:identity].nil? || evaluation_context[:segments].nil?

identity_segments = get_identity_segments_from_context(evaluation_context)

segments = identity_segments.map do |segment|
result = {
name: segment[:name]
}

result[:metadata] = segment[:metadata] if segment[:metadata]

result
end

segment_overrides = process_segment_overrides(identity_segments)

[segments, segment_overrides]
end

# Returns Record<string: override.name, SegmentOverride>
def process_segment_overrides(identity_segments)
segment_overrides = {}

identity_segments.each do |segment|
next unless segment[:overrides]

overrides_list = segment[:overrides].is_a?(Array) ? segment[:overrides] : []

overrides_list.each do |override|
next unless should_apply_override(override, segment_overrides)

segment_overrides[override[:name]] = {
feature: override,
segment_name: segment[:name]
}
end
end

segment_overrides
end

# returns EvaluationResultFlags<Metadata>
def evaluate_features(evaluation_context, segment_overrides)
flags = {}

(evaluation_context[:features] || {}).each_value do |feature|
segment_override = segment_overrides[feature[:name]]
final_feature = segment_override ? segment_override[:feature] : feature
has_override = !segment_override.nil?

# Evaluate feature value
evaluated = if has_override
{ value: final_feature[:value], reason: nil }
else
evaluate_feature_value(final_feature, get_identity_key(evaluation_context))
end

# Build flag result
flag_result = {
name: final_feature[:name],
enabled: final_feature[:enabled],
value: evaluated[:value]
}

# Add metadata if present
flag_result[:metadata] = final_feature[:metadata] if final_feature[:metadata]

# Set reason
flag_result[:reason] = evaluated[:reason] ||
get_targeting_match_reason({ type: 'SEGMENT', override: segment_override })

flags[final_feature[:name].to_sym] = flag_result
end

flags
end

# Returns {value: any; reason?: string}
def evaluate_feature_value(feature, identity_key = nil)
return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key

{ value: feature[:value], reason: nil }
end

# Returns {value: any; reason?: string}
def get_multivariate_feature_value(feature, identity_key)
percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key])
sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || Float::INFINITY }

start_percentage = 0
sorted_variants.each do |variant|
limit = start_percentage + variant[:weight]
if start_percentage <= percentage_value && percentage_value < limit
return {
value: variant[:value],
reason: get_targeting_match_reason({ type: 'SPLIT', weight: variant[:weight] })
}
end
start_percentage = limit
end

{ value: feature[:value], reason: nil }
end

# returns boolean
def should_apply_override(override, existing_overrides)
current_override = existing_overrides[override[:name]]
!current_override || higher_priority?(override[:priority], current_override[:feature][:priority])
end

private

# Extract identity key from evaluation context
#
# @param evaluation_context [Hash] The evaluation context
# @return [String, nil] The identity key or nil if no identity
def get_identity_key(evaluation_context)
return nil unless evaluation_context[:identity]

evaluation_context[:identity][:key] ||
"#{evaluation_context[:environment][:key]}_#{evaluation_context[:identity][:identifier]}"
end

# returns boolean
def higher_priority?(priority_a, priority_b)
(priority_a || Float::INFINITY) < (priority_b || Float::INFINITY)
end

def get_targeting_match_reason(match_object)
type = match_object[:type]

if type == 'SEGMENT'
return match_object[:override] ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{match_object[:override][:segment_name]}" : TARGETING_REASON_DEFAULT
end

return "#{TARGETING_REASON_SPLIT}; weight=#{match_object[:weight]}" if type == 'SPLIT'

TARGETING_REASON_DEFAULT
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/flagsmith/engine/evaluation/mappers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def self.map_identity_overrides_to_segments(identity_overrides)
features_to_identifiers = {}

identity_overrides.each do |identity|
next if identity.identity_features.nil? || !identity.identity_features.any?
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 }
Expand Down
14 changes: 14 additions & 0 deletions lib/flagsmith/engine/features/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module Flagsmith
module Engine
module Features
# Targeting reason constants for evaluation results
module TargetingReasons
TARGETING_REASON_DEFAULT = 'DEFAULT'
TARGETING_REASON_TARGETING_MATCH = 'TARGETING_MATCH'
TARGETING_REASON_SPLIT = 'SPLIT'
end
end
end
end
Loading