1+ # frozen_string_literal: true
2+
3+ module Flagsmith
4+ module Engine
5+ module EvaluationContext
6+ module Mappers
7+ # Using integer constant instead of -Float::INFINITY because the JSON serializer rejects infinity values
8+ HIGHEST_PRIORITY = 0
9+ WEAKEST_PRIORITY = 99_999_999
10+ #
11+ # @param environment [Flagsmith::Engine::Environment] The environment model
12+ # @param identity [Flagsmith::Engine::Identity, nil] Optional identity model
13+ # @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
14+ # @return [Hash] Evaluation context with environment, features, segments, and optionally identity
15+ def self . get_evaluation_context ( environment , identity = nil , override_traits = nil )
16+ environment_context = map_environment_model_to_evaluation_context ( environment )
17+ identity_context = identity ? map_identity_model_to_identity_context ( identity , override_traits ) : nil
18+
19+ context = environment_context . dup
20+ context [ :identity ] = identity_context if identity_context
21+
22+ context
23+ end
24+
25+ # Maps environment model to evaluation context
26+ #
27+ # @param environment [Flagsmith::Engine::Environment] The environment model
28+ # @return [Hash] Context with :environment, :features, and :segments keys
29+ def self . map_environment_model_to_evaluation_context ( environment )
30+ environment_context = {
31+ key : environment . api_key ,
32+ name : environment . project . name
33+ }
34+
35+ # Map feature states to features hash
36+ features = { }
37+ environment . feature_states . each do |fs |
38+ # Map multivariate values if present
39+ variants = nil
40+ if fs . multivariate_feature_state_values &.any?
41+ variants = fs . multivariate_feature_state_values . map do |mv |
42+ {
43+ value : mv . multivariate_feature_option . value ,
44+ weight : mv . percentage_allocation ,
45+ priority : mv . id || uuid_to_big_int ( mv . mv_fs_value_uuid )
46+ }
47+ end
48+ end
49+
50+ feature_hash = {
51+ key : fs . django_id &.to_s || fs . uuid ,
52+ feature_key : fs . feature . id . to_s ,
53+ name : fs . feature . name ,
54+ enabled : fs . enabled ,
55+ value : fs . get_value
56+ }
57+
58+ feature_hash [ :variants ] = variants if variants
59+ feature_hash [ :priority ] = fs . feature_segment . priority if fs . feature_segment &.priority
60+ feature_hash [ :metadata ] = { flagsmithId : fs . feature . id }
61+
62+ features [ fs . feature . name ] = feature_hash
63+ end
64+
65+ # Map segments from project
66+ segments = { }
67+ environment . project . segments . each do |segment |
68+ overrides = segment . feature_states . map do |fs |
69+ override_hash = {
70+ key : fs . django_id &.to_s || fs . uuid ,
71+ feature_key : fs . feature . id . to_s ,
72+ name : fs . feature . name ,
73+ enabled : fs . enabled ,
74+ value : fs . get_value
75+ }
76+ override_hash [ :priority ] = fs . feature_segment . priority if fs . feature_segment &.priority
77+ override_hash
78+ end
79+
80+ segments [ segment . id . to_s ] = {
81+ key : segment . id . to_s ,
82+ name : segment . name ,
83+ rules : segment . rules . map { |rule | map_segment_rule_model_to_rule ( rule ) } ,
84+ overrides : overrides ,
85+ metadata : {
86+ source : 'API' ,
87+ flagsmith_id : segment . id
88+ }
89+ }
90+ end
91+
92+ # Map identity overrides to segments
93+ if environment . identity_overrides &.any?
94+ identity_override_segments = map_identity_overrides_to_segments ( environment . identity_overrides )
95+ segments . merge! ( identity_override_segments )
96+ end
97+
98+ {
99+ environment : environment_context ,
100+ features : features ,
101+ segments : segments
102+ }
103+ end
104+
105+ def self . uuid_to_big_int ( uuid )
106+ uuid . gsub ( '-' , '' ) . to_i ( 16 )
107+ end
108+
109+ # Maps identity model to identity context
110+ #
111+ # @param identity [Flagsmith::Engine::Identity] The identity model
112+ # @param override_traits [Array<Flagsmith::Engine::Identities::Trait>, nil] Optional override traits
113+ # @return [Hash] Identity context with :identifier, :key, and :traits
114+ def self . map_identity_model_to_identity_context ( identity , override_traits = nil )
115+ # Use override traits if provided, otherwise use identity's traits
116+ traits = override_traits || identity . identity_traits
117+
118+ # Map traits to a hash with trait key => trait value
119+ traits_hash = { }
120+ traits . each do |trait |
121+ traits_hash [ trait . trait_key ] = trait . trait_value
122+ end
123+
124+ {
125+ identifier : identity . identifier ,
126+ key : identity . django_id &.to_s || identity . composite_key ,
127+ traits : traits_hash
128+ }
129+ end
130+
131+ # Maps segment rule model to rule hash
132+ #
133+ # @param rule [Flagsmith::Engine::Segments::Rule] The segment rule model
134+ # @return [Hash] Mapped rule with :type, :conditions, and :rules
135+ def self . map_segment_rule_model_to_rule ( rule )
136+ result = {
137+ type : rule . type
138+ }
139+
140+ # Map conditions if present
141+ if rule . conditions &.any?
142+ result [ :conditions ] = rule . conditions . map do |condition |
143+ {
144+ property : condition . property ,
145+ operator : condition . operator ,
146+ value : condition . value
147+ }
148+ end
149+ else
150+ result [ :conditions ] = [ ]
151+ end
152+
153+ if rule . rules &.any?
154+ result [ :rules ] = rule . rules . map { |nested_rule | map_segment_rule_model_to_rule ( nested_rule ) }
155+ else
156+ result [ :rules ] = [ ]
157+ end
158+
159+ result
160+ end
161+
162+ # Maps identity overrides to segments
163+ #
164+ # @param identity_overrides [Array<Flagsmith::Engine::Identity>] Array of identity override models
165+ # @return [Hash] Segments hash for identity overrides
166+ def self . map_identity_overrides_to_segments ( identity_overrides )
167+ require 'digest'
168+
169+ segments = { }
170+ features_to_identifiers = { }
171+
172+ identity_overrides . each do |identity |
173+ next if identity . identity_features . nil? || !identity . identity_features . any?
174+
175+ # Sort features by name for consistent hashing
176+ sorted_features = identity . identity_features . to_a . sort_by { |fs | fs . feature . name }
177+
178+ # Create override keys for hashing
179+ overrides_key = sorted_features . map do |fs |
180+ {
181+ feature_key : fs . feature . id . to_s ,
182+ name : fs . feature . name ,
183+ enabled : fs . enabled ,
184+ value : fs . get_value ,
185+ priority : WEAKEST_PRIORITY ,
186+ metadata : {
187+ flagsmithId : fs . feature . id
188+ }
189+ }
190+ end
191+
192+ # Create hash of the overrides to group identities with same overrides
193+ overrides_hash = Digest ::SHA1 . hexdigest ( overrides_key . to_json )
194+
195+ features_to_identifiers [ overrides_hash ] ||= { identifiers : [ ] , overrides : overrides_key }
196+ features_to_identifiers [ overrides_hash ] [ :identifiers ] << identity . identifier
197+ end
198+
199+ # Create segments for each unique set of overrides
200+ features_to_identifiers . each do |overrides_hash , data |
201+ segment_key = "identity_override_#{ overrides_hash } "
202+
203+ segments [ segment_key ] = {
204+ key : segment_key ,
205+ name : 'identity_override' ,
206+ rules : [
207+ {
208+ type : 'ALL' ,
209+ conditions : [
210+ {
211+ property : '$.identity.identifier' ,
212+ operator : 'IN' ,
213+ value : data [ :identifiers ] . join ( ',' )
214+ }
215+ ] ,
216+ rules : [ ]
217+ }
218+ ] ,
219+ metadata : {
220+ source : 'identity_override'
221+ } ,
222+ overrides : data [ :overrides ]
223+ }
224+ end
225+
226+ segments
227+ end
228+ end
229+ end
230+ end
231+ end
232+
0 commit comments