Skip to content

Commit fc1369a

Browse files
Zaimwa9khvn26
andauthored
feat!: sdk consumes context engine (#89)
* fix: sdk-uses-new-engine-methods * feat: introduced-jsonpath-library * feat: fixed-conflict * Update lib/flagsmith/engine/segments/models.rb Co-authored-by: Kim Gustyr <[email protected]> * Update lib/flagsmith/engine/segments/models.rb * feat: removed-normalize * feat: linter * feat: replaced-flagsmith-id-with-id * feat: removed-comments --------- Co-authored-by: Kim Gustyr <[email protected]>
1 parent 080f191 commit fc1369a

File tree

12 files changed

+182
-266
lines changed

12 files changed

+182
-266
lines changed

lib/flagsmith.rb

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def initialize(config)
6969
api_client
7070
analytics_processor
7171
environment_data_polling_manager
72-
engine
7372
load_offline_handler
7473
end
7574

@@ -99,10 +98,6 @@ def realtime_client
9998
@realtime_client ||= Flagsmith::RealtimeClient.new(@config)
10099
end
101100

102-
def engine
103-
@engine ||= Flagsmith::Engine::Engine.new
104-
end
105-
106101
def analytics_processor
107102
return nil unless @config.enable_analytics?
108103

@@ -211,21 +206,33 @@ def get_value_for_identity(feature_name, user_id = nil, default: nil)
211206
end
212207

213208
def get_identity_segments(identifier, traits = {})
214-
unless environment
215-
raise Flagsmith::ClientError,
216-
'Local evaluation or offline handler is required to obtain identity segments.'
217-
end
209+
raise Flagsmith::ClientError, 'Local evaluation or offline handler is required to obtain identity segments.' unless environment
218210

219211
identity_model = get_identity_model(identifier, traits)
220-
segment_models = engine.get_identity_segments(environment, identity_model)
221-
segment_models.map { |sm| Flagsmith::Segments::Segment.new(id: sm.id, name: sm.name) }.compact
212+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
213+
raise Flagsmith::ClientError, 'Local evaluation required to obtain identity segments' unless context
214+
215+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
216+
evaluation_result[:segments].filter_map do |segment_result|
217+
id = segment_result.dig(:metadata, :id)
218+
Flagsmith::Segments::Segment.new(id: id, name: segment_result[:name]) if id
219+
end
222220
end
223221

224222
private
225223

226-
def environment_flags_from_document
227-
Flagsmith::Flags::Collection.from_feature_state_models(
228-
engine.get_environment_feature_states(environment),
224+
def environment_flags_from_document # rubocop:disable Metrics/MethodLength
225+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment)
226+
227+
unless context
228+
raise Flagsmith::ClientError,
229+
'Unable to get flags. No environment present.'
230+
end
231+
232+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
233+
234+
Flagsmith::Flags::Collection.from_evaluation_result(
235+
evaluation_result,
229236
analytics_processor: analytics_processor,
230237
default_flag_handler: default_flag_handler,
231238
offline_handler: offline_handler
@@ -234,12 +241,13 @@ def environment_flags_from_document
234241

235242
def get_identity_flags_from_document(identifier, traits = {})
236243
identity_model = get_identity_model(identifier, traits)
244+
context = Flagsmith::Engine::Mappers.get_evaluation_context(environment, identity_model)
245+
raise Flagsmith::ClientError, 'Unable to get flags. No environment present.' unless context
237246

238-
Flagsmith::Flags::Collection.from_feature_state_models(
239-
engine.get_identity_feature_states(environment, identity_model),
240-
identity_id: identity_model.composite_key,
241-
analytics_processor: analytics_processor,
242-
default_flag_handler: default_flag_handler,
247+
evaluation_result = Flagsmith::Engine.get_evaluation_result(context)
248+
Flagsmith::Flags::Collection.from_evaluation_result(
249+
evaluation_result,
250+
analytics_processor: analytics_processor, default_flag_handler: default_flag_handler,
243251
offline_handler: offline_handler
244252
)
245253
end

lib/flagsmith/engine/core.rb

Lines changed: 106 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,86 +5,143 @@
55

66
require_relative 'environments/models'
77
require_relative 'features/models'
8+
require_relative 'features/constants'
89
require_relative 'identities/models'
910
require_relative 'organisations/models'
1011
require_relative 'projects/models'
1112
require_relative 'segments/evaluator'
1213
require_relative 'segments/models'
1314
require_relative 'utils/hash_func'
1415
require_relative 'mappers'
15-
require_relative 'evaluation/core'
1616

1717
module Flagsmith
18+
# Core evaluation logic for feature flags
1819
module Engine
19-
# Flags engine methods
20-
class Engine
21-
include Flagsmith::Engine::Segments::Evaluator
22-
23-
def get_identity_feature_state(environment, identity, feature_name, override_traits = nil)
24-
feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values
20+
extend self
21+
include Flagsmith::Engine::Utils::HashFunc
22+
include Flagsmith::Engine::Features::TargetingReasons
23+
include Flagsmith::Engine::Segments::Evaluator
24+
25+
# Get evaluation result from evaluation context
26+
#
27+
# @param evaluation_context [Hash] The evaluation context
28+
# @return [Hash] Evaluation result with flags and segments
29+
def get_evaluation_result(evaluation_context)
30+
evaluation_context = get_enriched_context(evaluation_context)
31+
segments, segment_overrides = evaluate_segments(evaluation_context)
32+
flags = evaluate_features(evaluation_context, segment_overrides)
33+
{
34+
flags: flags,
35+
segments: segments
36+
}
37+
end
2538

26-
feature_state = feature_states.find { |f| f.feature.name == feature_name }
39+
# Returns { segments: EvaluationResultSegments; segmentOverrides: Record<string, SegmentOverride>; }
40+
def evaluate_segments(evaluation_context)
41+
return [], {} if evaluation_context[:segments].nil?
2742

28-
raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if feature_state.nil?
43+
identity_segments = get_segments_from_context(evaluation_context)
2944

30-
feature_state
45+
segments = identity_segments.map do |segment|
46+
{ name: segment[:name], metadata: segment[:metadata] }.compact
3147
end
3248

33-
def get_identity_feature_states(environment, identity, override_traits = nil)
34-
feature_states = get_identity_feature_states_dict(environment, identity, override_traits).values
49+
segment_overrides = process_segment_overrides(identity_segments)
50+
51+
[segments, segment_overrides]
52+
end
3553

36-
return feature_states.select(&:enabled?) if environment.project.hide_disabled_flags
54+
# Returns Record<string: override.name, SegmentOverride>
55+
def process_segment_overrides(identity_segments) # rubocop:disable Metrics/MethodLength
56+
segment_overrides = {}
3757

38-
feature_states
58+
identity_segments.each do |segment|
59+
Array(segment[:overrides]).each do |override|
60+
next unless should_apply_override(override, segment_overrides)
61+
62+
segment_overrides[override[:name]] = {
63+
feature: override,
64+
segment_name: segment[:name]
65+
}
66+
end
3967
end
4068

41-
def get_environment_feature_state(environment, feature_name)
42-
features_state = environment.feature_states.find { |f| f.feature.name == feature_name }
69+
segment_overrides
70+
end
4371

44-
raise Flagsmith::FeatureStateNotFound, 'Feature State Not Found' if features_state.nil?
72+
def evaluate_features(evaluation_context, segment_overrides)
73+
identity_key = get_identity_key(evaluation_context)
4574

46-
features_state
75+
(evaluation_context[:features] || {}).each_with_object({}) do |(_, feature), flags|
76+
segment_override = segment_overrides[feature[:name]]
77+
final_feature = segment_override ? segment_override[:feature] : feature
78+
79+
flag_result = build_flag_result(final_feature, identity_key, segment_override)
80+
flags[final_feature[:name].to_sym] = flag_result
4781
end
82+
end
4883

49-
def get_environment_feature_states(environment)
50-
return environment.feature_states.select(&:enabled?) if environment.project.hide_disabled_flags
84+
# Returns {value: any; reason?: string}
85+
def evaluate_feature_value(feature, identity_key = nil)
86+
return get_multivariate_feature_value(feature, identity_key) if feature[:variants]&.any? && identity_key
5187

52-
environment.feature_states
53-
end
88+
{ value: feature[:value], reason: nil }
89+
end
5490

55-
private
91+
# Returns {value: any; reason?: string}
92+
def get_multivariate_feature_value(feature, identity_key)
93+
percentage_value = hashed_percentage_for_object_ids([feature[:key], identity_key])
94+
sorted_variants = (feature[:variants] || []).sort_by { |v| v[:priority] || WEAKEST_PRIORITY }
5695

57-
def get_identity_feature_states_dict(environment, identity, override_traits = nil)
58-
# Get feature states from the environment
59-
feature_states = {}
60-
override = ->(fs) { feature_states[fs.feature.id] = fs }
61-
environment.feature_states.each(&override)
96+
variant = find_matching_variant(sorted_variants, percentage_value)
97+
variant || { value: feature[:value], reason: nil }
98+
end
6299

63-
override_by_matching_segments(environment, identity, override_traits) do |fs|
64-
override.call(fs) unless higher_segment_priority?(feature_states, fs)
65-
end
100+
def find_matching_variant(sorted_variants, percentage_value)
101+
start_percentage = 0
102+
sorted_variants.each do |variant|
103+
limit = start_percentage + variant[:weight]
104+
return { value: variant[:value], reason: "#{TARGETING_REASON_SPLIT}; weight=#{variant[:weight]}" } if start_percentage <= percentage_value && percentage_value < limit
66105

67-
# Override with any feature states defined directly the identity
68-
identity.identity_features.each(&override)
69-
feature_states
106+
start_percentage = limit
70107
end
108+
nil
109+
end
71110

72-
# Override with any feature states defined by matching segments
73-
def override_by_matching_segments(environment, identity, override_traits)
74-
identity_segments = get_identity_segments(environment, identity, override_traits)
75-
identity_segments.each do |matching_segment|
76-
matching_segment.feature_states.each do |feature_state|
77-
yield feature_state if block_given?
78-
end
79-
end
80-
end
111+
# returns boolean
112+
def should_apply_override(override, existing_overrides)
113+
current_override = existing_overrides[override[:name]]
114+
!current_override || stronger_priority?(override[:priority], current_override[:feature][:priority])
115+
end
81116

82-
def higher_segment_priority?(collection, feature_state)
83-
collection.key?(feature_state.feature.id) &&
84-
collection[feature_state.feature.id].higher_segment_priority?(
85-
feature_state
86-
)
87-
end
117+
private
118+
119+
def build_flag_result(feature, identity_key, segment_override)
120+
evaluated = evaluate_feature_value(feature, identity_key)
121+
122+
flag_result = {
123+
name: feature[:name],
124+
enabled: feature[:enabled],
125+
value: evaluated[:value],
126+
reason: evaluated[:reason] || (segment_override ? "#{TARGETING_REASON_TARGETING_MATCH}; segment=#{segment_override[:segment_name]}" : TARGETING_REASON_DEFAULT)
127+
}
128+
129+
flag_result[:metadata] = feature[:metadata] if feature[:metadata]
130+
flag_result
131+
end
132+
133+
# Extract identity key from evaluation context
134+
#
135+
# @param evaluation_context [Hash] The evaluation context
136+
# @return [String, nil] The identity key or nil if no identity
137+
def get_identity_key(evaluation_context)
138+
return nil unless evaluation_context[:identity]
139+
140+
evaluation_context[:identity][:key]
141+
end
142+
143+
def stronger_priority?(priority_a, priority_b)
144+
(priority_a || WEAKEST_PRIORITY) < (priority_b || WEAKEST_PRIORITY)
88145
end
89146
end
90147
end

lib/flagsmith/engine/features/models.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def multivariate_value(identity_id)
5858
# but `self` does.
5959
# 2. `other` have a feature segment with high priority
6060
def higher_segment_priority?(other)
61-
feature_segment.priority.to_i < (other&.feature_segment&.priority || Float::INFINITY)
61+
feature_segment.priority.to_i < (other&.feature_segment&.priority || WEAKEST_PRIORITY)
6262
rescue TypeError, NoMethodError
6363
false
6464
end

lib/flagsmith/engine/mappers/environment.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def self.build_feature_hash(feature_state)
2626
name: feature_state.feature.name,
2727
enabled: feature_state.enabled,
2828
value: feature_state.get_value,
29-
metadata: { flagsmith_id: feature_state.feature.id }
29+
metadata: { id: feature_state.feature.id }
3030
}
3131
add_variants_to_feature(feature_hash, feature_state)
3232
add_priority_to_feature(feature_hash, feature_state)

lib/flagsmith/engine/mappers/identity.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def self.build_overrides_key(identity_features)
5252
enabled: feature_state.enabled,
5353
value: feature_state.get_value,
5454
priority: Mappers::STRONGEST_PRIORITY,
55-
metadata: { flagsmith_id: feature_state.feature.id }
55+
metadata: { id: feature_state.feature.id }
5656
}
5757
end
5858
end

lib/flagsmith/engine/mappers/segments.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def self.build_segment_hash(segment)
2121
overrides: build_overrides(segment.feature_states),
2222
metadata: {
2323
source: 'API',
24-
flagsmith_id: segment.id
24+
id: segment.id
2525
}
2626
}
2727
end
@@ -33,7 +33,7 @@ def self.build_overrides(feature_states) # rubocop:disable Metrics/MethodLength
3333
name: feature_state.feature.name,
3434
enabled: feature_state.enabled,
3535
value: feature_state.get_value,
36-
metadata: { flagsmith_id: feature_state.feature.id }
36+
metadata: { id: feature_state.feature.id }
3737
}
3838
add_priority_to_override(override_hash, feature_state)
3939
override_hash

lib/flagsmith/engine/segments/evaluator.rb

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,6 @@ module Evaluator # rubocop:disable Metrics/ModuleLength
1414
include Flagsmith::Engine::Segments::Constants
1515
include Flagsmith::Engine::Utils::HashFunc
1616

17-
# Model-based segment evaluation
18-
def get_identity_segments(environment, identity, override_traits = nil)
19-
environment.project.segments.select do |s|
20-
evaluate_identity_in_segment(identity, s, override_traits)
21-
end
22-
end
23-
24-
def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
25-
if condition.operator == PERCENTAGE_SPLIT
26-
return hashed_percentage_for_object_ids([segment_id,
27-
identity_id]) <= condition.value.to_f
28-
end
29-
30-
trait = identity_traits.find { |t| t.key.to_s == condition.property }
31-
32-
return handle_trait_existence_conditions(trait, condition.operator) if [IS_SET,
33-
IS_NOT_SET].include?(condition.operator)
34-
35-
return condition.match_trait_value?(trait.trait_value) if trait
36-
37-
false
38-
end
39-
4017
module_function
4118

4219
def get_enriched_context(context)
@@ -67,6 +44,22 @@ def get_segments_from_context(context)
6744
end
6845
end
6946

47+
def traits_match_segment_condition(identity_traits, condition, segment_id, identity_id)
48+
if condition.operator == PERCENTAGE_SPLIT
49+
return hashed_percentage_for_object_ids([segment_id,
50+
identity_id]) <= condition.value.to_f
51+
end
52+
53+
trait = identity_traits.find { |t| t.key.to_s == condition.property }
54+
55+
return handle_trait_existence_conditions(trait, condition.operator) if [IS_SET,
56+
IS_NOT_SET].include?(condition.operator)
57+
58+
return condition.match_trait_value?(trait.trait_value) if trait
59+
60+
false
61+
end
62+
7063
# Evaluates whether a given identity is in the provided segment.
7164
#
7265
# :param identity: identity model object to evaluate
@@ -248,14 +241,6 @@ def primitive?(value)
248241

249242
!(value.is_a?(Hash) || value.is_a?(Array))
250243
end
251-
252-
private
253-
254-
def handle_trait_existence_conditions(matching_trait, operator)
255-
return operator == IS_NOT_SET if matching_trait.nil?
256-
257-
operator == IS_SET
258-
end
259244
end
260245
end
261246
end

0 commit comments

Comments
 (0)