Skip to content

Commit bde89c2

Browse files
committed
feat: rebased
2 parents cefe981 + 080f191 commit bde89c2

File tree

1 file changed

+142
-0
lines changed
  • lib/flagsmith/engine/evaluation

1 file changed

+142
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../utils/hash_func'
4+
require_relative '../features/constants'
5+
require_relative '../segments/evaluator'
6+
7+
module Flagsmith
8+
module Engine
9+
module Evaluation
10+
# Core evaluation logic for feature flags
11+
module Core
12+
extend self
13+
include Flagsmith::Engine::Utils::HashFunc
14+
include Flagsmith::Engine::Features::TargetingReasons
15+
include Flagsmith::Engine::Segments::Evaluator
16+
# Get evaluation result from evaluation context
17+
#
18+
# @param evaluation_context [Hash] The evaluation context
19+
# @return [Hash] Evaluation result with flags and segments
20+
def get_evaluation_result(evaluation_context)
21+
evaluation_context = get_enriched_context(evaluation_context)
22+
segments, segment_overrides = evaluate_segments(evaluation_context)
23+
flags = evaluate_features(evaluation_context, segment_overrides)
24+
{
25+
flags: flags,
26+
segments: segments
27+
}
28+
end
29+
30+
# Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; }
31+
def evaluate_segments(evaluation_context)
32+
return [], {} if evaluation_context[:segments].nil?
33+
34+
identity_segments = get_segments_from_context(evaluation_context)
35+
36+
segments = identity_segments.map do |segment|
37+
{ name: segment[:name], metadata: segment[:metadata] }.compact
38+
end
39+
40+
segment_overrides = process_segment_overrides(identity_segments)
41+
42+
[segments, segment_overrides]
43+
end
44+
45+
# Returns Record<string: override.name, SegmentOverride>
46+
def process_segment_overrides(identity_segments) # rubocop:disable Metrics/MethodLength
47+
segment_overrides = {}
48+
49+
identity_segments.each do |segment|
50+
Array(segment[:overrides]).each do |override|
51+
next unless should_apply_override(override, segment_overrides)
52+
53+
segment_overrides[override[:name]] = {
54+
feature: override,
55+
segment_name: segment[:name]
56+
}
57+
end
58+
end
59+
60+
segment_overrides
61+
end
62+
63+
def evaluate_features(evaluation_context, segment_overrides)
64+
identity_key = get_identity_key(evaluation_context)
65+
66+
(evaluation_context[:features] || {}).each_with_object({}) do |(_, feature), flags|
67+
segment_override = segment_overrides[feature[:name]]
68+
final_feature = segment_override ? segment_override[:feature] : feature
69+
70+
flag_result = build_flag_result(final_feature, identity_key, segment_override)
71+
flags[final_feature[:name].to_sym] = flag_result
72+
end
73+
end
74+
75+
# Returns {value: any; reason?: string}
76+
def evaluate_feature_value(feature, identity_key = nil)
77+
return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key
78+
79+
{ value: feature[:value], reason: nil }
80+
end
81+
82+
# Returns {value: any; reason?: string}
83+
def get_multivariate_feature_value(feature, identity_key)
84+
percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key])
85+
sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY }
86+
87+
variant = find_matching_variant(sorted_variants, percentage_value)
88+
variant || { value: feature[:value], reason: nil }
89+
end
90+
91+
def find_matching_variant(sorted_variants, percentage_value)
92+
start_percentage = 0
93+
sorted_variants.each do |variant|
94+
limit = start_percentage + variant[:weight]
95+
return { value: variant[:value], reason: "#{TARGETING_REASON_SPLIT}; weight=#{variant[:weight]}" } if start_percentage <= percentage_value && percentage_value < limit
96+
97+
start_percentage = limit
98+
end
99+
nil
100+
end
101+
102+
# returns boolean
103+
def should_apply_override(override, existing_overrides)
104+
current_override = existing_overrides[override[:name]]
105+
!current_override || stronger_priority?(override[:priority], current_override[:feature][:priority])
106+
end
107+
108+
private
109+
110+
def build_flag_result(feature, identity_key, segment_override)
111+
evaluated = evaluate_feature_value(feature, identity_key)
112+
113+
flag_result = {
114+
name: feature[:name],
115+
enabled: feature[:enabled],
116+
value: evaluated[:value],
117+
reason: evaluated[:reason] || (segment_override ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{segment_override[:segment_name]}" : TARGETING_REASON_DEFAULT)
118+
}
119+
120+
flag_result[:metadata] = feature[:metadata] if feature[:metadata]
121+
flag_result
122+
end
123+
124+
# Extract identity key from evaluation context
125+
#
126+
# @param evaluation_context [Hash] The evaluation context
127+
# @return [String, nil] The identity key or nil if no identity
128+
def get_identity_key(evaluation_context)
129+
return nil unless evaluation_context[:identity]
130+
131+
evaluation_context[:identity][:key] ||
132+
"#{evaluation_context[:environment][:key]}_#{evaluation_context[:identity][:identifier]}"
133+
end
134+
135+
# returns boolean
136+
def stronger_priority?(priority_a, priority_b)
137+
(priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY)
138+
end
139+
end
140+
end
141+
end
142+
end

0 commit comments

Comments
 (0)