-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathEngineMappers.java
More file actions
452 lines (389 loc) · 16.1 KB
/
EngineMappers.java
File metadata and controls
452 lines (389 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
package com.flagsmith.mappers;
import com.fasterxml.jackson.databind.JsonNode;
import com.flagsmith.MapperFactory;
import com.flagsmith.flagengine.EngineConstants;
import com.flagsmith.flagengine.EnvironmentContext;
import com.flagsmith.flagengine.EvaluationContext;
import com.flagsmith.flagengine.FeatureContext;
import com.flagsmith.flagengine.FeatureValue;
import com.flagsmith.flagengine.FlagResult;
import com.flagsmith.flagengine.IdentityContext;
import com.flagsmith.flagengine.SegmentCondition;
import com.flagsmith.flagengine.SegmentContext;
import com.flagsmith.flagengine.SegmentRule;
import com.flagsmith.flagengine.Segments;
import com.flagsmith.flagengine.Traits;
import com.flagsmith.flagengine.segments.constants.SegmentConditions;
import com.flagsmith.models.FeatureMetadata;
import com.flagsmith.models.Flag;
import com.flagsmith.models.SegmentMetadata;
import com.flagsmith.models.TraitModel;
import com.flagsmith.models.environments.EnvironmentModel;
import com.flagsmith.models.features.FeatureModel;
import com.flagsmith.models.features.FeatureSegmentModel;
import com.flagsmith.models.features.FeatureStateModel;
import com.flagsmith.models.features.MultivariateFeatureStateValueModel;
import com.flagsmith.models.identities.IdentityModel;
import com.flagsmith.models.projects.ProjectModel;
import com.flagsmith.models.segments.SegmentConditionModel;
import com.flagsmith.models.segments.SegmentModel;
import com.flagsmith.models.segments.SegmentRuleModel;
import com.flagsmith.utils.ModelUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* EngineMappers
*
* <p>Utility class for mapping JSON data to flag engine context objects.
*/
public class EngineMappers {
/**
* Maps FlagResult to Flag.
* Returns null if metadata is missing or invalid.
*
* @param flagResult the flag result
* @return the mapped flag or null
*/
public static Flag mapFlagResultToFlag(
FlagResult flagResult
) {
FeatureMetadata metadata;
metadata = MapperFactory.getMapper()
.convertValue(flagResult.getMetadata(), FeatureMetadata.class);
Flag flag = new Flag();
flag.setFeatureId(metadata.getFlagsmithId());
flag.setFeatureName(flagResult.getName());
flag.setValue(flagResult.getValue());
flag.setEnabled(flagResult.getEnabled());
return flag;
}
/**
* Maps context and identity data to evaluation context.
*
* @param context the base evaluation context
* @param identifier the identity identifier
* @param traits optional traits mapping
* @return the updated evaluation context with identity information
*/
public static EvaluationContext mapContextAndIdentityDataToContext(
EvaluationContext context,
String identifier,
Map<String, Object> traits) {
// Create identity context
IdentityContext identityContext = new IdentityContext()
.withIdentifier(identifier);
// Map traits if provided
if (traits != null && !traits.isEmpty()) {
Traits identityTraits = new Traits();
for (TraitModel traitModel : ModelUtils.getTraitModelsFromTraitMap(traits)) {
identityTraits.setAdditionalProperty(
traitModel.getTraitKey(), traitModel.getTraitValue());
}
identityContext.setTraits(identityTraits);
}
// Create new evaluation context with identity
return new EvaluationContext(context)
.withIdentity(identityContext);
}
/**
* Maps environment document to evaluation context.
*
* @param environmentDocument the environment document JSON
* @return the evaluation context
*/
public static EvaluationContext mapEnvironmentDocumentToContext(
JsonNode environmentDocument) {
return mapEnvironmentToContext(
MapperFactory.getMapper().convertValue(environmentDocument,
EnvironmentModel.class));
}
/**
* Maps environment model to evaluation context.
*
* @param environmentModel the environment model
* @return the evaluation context
*/
public static EvaluationContext mapEnvironmentToContext(
EnvironmentModel environmentModel) {
// Create environment context
final EnvironmentContext environmentContext = new EnvironmentContext()
.withKey(environmentModel.getApiKey())
.withName(environmentModel.getName());
// Map features
Map<String, FeatureContext> features = new HashMap<>();
for (FeatureStateModel featureState : environmentModel.getFeatureStates()) {
FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState);
features.put(featureContext.getName(), featureContext);
}
// Map segments
Map<String, SegmentContext> segments = new HashMap<>();
// Map project segments
ProjectModel project = environmentModel.getProject();
for (SegmentModel segment : project.getSegments()) {
String segmentKey = String.valueOf(segment.getId());
segments.put(segmentKey, mapSegmentToSegmentContext(segment));
}
// Map identity overrides
Map<String, SegmentContext> identityOverrideSegments = mapIdentityOverridesToSegments(
environmentModel.getIdentityOverrides());
segments.putAll(identityOverrideSegments);
// Create evaluation context
EvaluationContext evaluationContext = new EvaluationContext()
.withEnvironment(environmentContext);
// Add features individually
com.flagsmith.flagengine.Features featuresObj = new com.flagsmith.flagengine.Features();
for (Map.Entry<String, FeatureContext> entry : features.entrySet()) {
featuresObj.withAdditionalProperty(entry.getKey(), entry.getValue());
}
evaluationContext.withFeatures(featuresObj);
// Add segments individually
Segments segmentsObj = new Segments();
for (Map.Entry<String, SegmentContext> entry : segments.entrySet()) {
segmentsObj.withAdditionalProperty(entry.getKey(), entry.getValue());
}
evaluationContext.withSegments(segmentsObj);
return evaluationContext;
}
/**
* Maps identity overrides to segment contexts.
*
* @param identityOverrides the identity overrides JSON array
* @return map of segment contexts
*/
private static Map<String, SegmentContext> mapIdentityOverridesToSegments(
List<IdentityModel> identityOverrides) {
// Map from sorted list of feature contexts to identifiers
Map<List<FeatureContext>, List<String>> featuresToIdentifiers = new HashMap<>();
for (IdentityModel identityOverride : identityOverrides) {
List<FeatureStateModel> identityFeatures = identityOverride.getIdentityFeatures();
if (identityFeatures == null || identityFeatures.isEmpty()) {
continue;
}
// Create overrides key as a sorted list of FeatureContext objects
List<FeatureContext> overridesKey = new ArrayList<>();
List<FeatureStateModel> sortedFeatures = new ArrayList<>();
identityFeatures.forEach(sortedFeatures::add);
sortedFeatures.sort((a, b) -> a.getFeature().getName()
.compareTo(b.getFeature().getName()));
for (FeatureStateModel featureState : sortedFeatures) {
FeatureModel feature = featureState.getFeature();
FeatureContext featureContext = new FeatureContext()
.withKey("")
.withName(feature.getName())
.withEnabled(featureState.getEnabled())
.withValue(featureState.getValue())
.withPriority(EngineConstants.STRONGEST_PRIORITY)
.withMetadata(mapFeatureStateToFeatureMetadata(featureState));
overridesKey.add(featureContext);
}
String identifier = identityOverride.getIdentifier();
featuresToIdentifiers.computeIfAbsent(overridesKey, k -> new ArrayList<>()).add(identifier);
}
Map<String, SegmentContext> segmentContexts = new HashMap<>();
for (Map.Entry<List<FeatureContext>, List<String>> entry : featuresToIdentifiers.entrySet()) {
List<FeatureContext> overridesKey = entry.getKey();
List<String> identifiers = entry.getValue();
String segmentKey = getVirtualSegmentKey(overridesKey);
// Create segment condition for identifier check
SegmentCondition identifierCondition = new SegmentCondition()
.withProperty("$.identity.identifier")
.withOperator(SegmentConditions.IN)
.withValue(identifiers);
// Create segment rule
SegmentRule segmentRule = new SegmentRule()
.withType(SegmentRule.Type.ALL)
.withConditions(List.of(identifierCondition));
// Create overrides from FeatureContext objects
List<FeatureContext> overrides = new ArrayList<>();
for (FeatureContext featureContext : overridesKey) {
// Copy the feature context for the override
FeatureContext override = new FeatureContext(featureContext)
.withKey(""); // Identity overrides never carry multivariate options
overrides.add(override);
}
SegmentMetadata metadata = new SegmentMetadata();
metadata.setSource(SegmentMetadata.Source.IDENTITY_OVERRIDES);
Map<String, Object> metadataMap = mapSegmentMetadataToMetadataMap(metadata);
SegmentContext segmentContext = new SegmentContext()
.withKey("") // Identity override segments never use % Split operator
.withName("identity_overrides")
.withRules(List.of(segmentRule))
.withOverrides(overrides)
.withMetadata(metadataMap);
segmentContexts.put(segmentKey, segmentContext);
}
return segmentContexts;
}
/**
* Maps environment document rules to context rules.
*
* @param rules the rules JSON array
* @return list of segment rules
*/
private static List<SegmentRule> mapEnvironmentDocumentRulesToContextRules(
List<SegmentRuleModel> rules) {
List<SegmentRule> segmentRules = new ArrayList<>();
for (SegmentRuleModel rule : rules) {
// Map conditions
List<SegmentCondition> conditions = new ArrayList<>();
if (rule.getConditions() != null) {
for (SegmentConditionModel condition : rule.getConditions()) {
SegmentCondition segmentCondition = new SegmentCondition()
.withProperty(condition.getProperty())
.withOperator(condition.getOperator())
.withValue(condition.getValue());
conditions.add(segmentCondition);
}
}
// Map sub-rules recursively
List<SegmentRule> subRules = mapEnvironmentDocumentRulesToContextRules(
rule.getRules());
SegmentRule segmentRule = new SegmentRule()
.withType(SegmentRule.Type.fromValue(rule.getType()))
.withConditions(conditions)
.withRules(subRules);
segmentRules.add(segmentRule);
}
return segmentRules;
}
/**
* Maps environment document feature states to feature contexts.
*
* @param featureStates the feature states JSON array
* @return list of feature contexts
*/
private static List<FeatureContext> mapEnvironmentDocumentFeatureStatesToFeatureContexts(
List<FeatureStateModel> featureStates) {
List<FeatureContext> featureContexts = new ArrayList<>();
if (featureStates != null) {
for (FeatureStateModel featureState : featureStates) {
FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState);
featureContexts.add(featureContext);
}
}
return featureContexts;
}
/**
* Gets the feature state key from either django_id or featurestate_uuid.
*
* @param featureState the feature state JSON
* @return the feature state key as string
*/
private static String getFeatureStateKey(FeatureStateModel featureState) {
Integer djangoId = featureState.getDjangoId();
if (djangoId != null) {
return djangoId.toString();
}
return featureState.getFeaturestateUuid();
}
private static double getMultivariateFeatureValuePriority(
MultivariateFeatureStateValueModel multivariateValue) {
if (multivariateValue.getId() != null) {
return multivariateValue.getId();
}
// Fallback to mv_fs_value_uuid if id is not present
UUID mvFsValueUuid = UUID.fromString(multivariateValue.getMvFsValueUuid());
return mvFsValueUuid.getMostSignificantBits() & Long.MAX_VALUE;
}
/**
* Maps a single feature state to feature context.
*
* @param featureState the feature state JSON
* @return the feature context
*/
private static FeatureContext mapFeatureStateToFeatureContext(FeatureStateModel featureState) {
FeatureContext featureContext = new FeatureContext()
.withKey(getFeatureStateKey(featureState))
.withName(featureState.getFeature().getName())
.withEnabled(featureState.getEnabled())
.withValue(featureState.getValue())
.withMetadata(mapFeatureStateToFeatureMetadata(featureState));
// Handle multivariate feature state values
List<FeatureValue> variants = new ArrayList<>();
for (MultivariateFeatureStateValueModel mvValue :
featureState.getMultivariateFeatureStateValues()) {
FeatureValue variant = new FeatureValue()
.withValue(mvValue.getMultivariateFeatureOption().getValue())
.withWeight(mvValue.getPercentageAllocation().doubleValue())
.withPriority(getMultivariateFeatureValuePriority(mvValue));
variants.add(variant);
}
featureContext.setVariants(variants);
// Handle priority from feature segment
FeatureSegmentModel featureSegment = featureState.getFeatureSegment();
if (featureSegment != null) {
Double priority = (double) featureSegment.getPriority();
if (priority != null) {
featureContext.withPriority(priority);
}
}
return featureContext;
}
private static Map<String, Object> mapFeatureStateToFeatureMetadata(
FeatureStateModel featureState) {
FeatureMetadata metadata = new FeatureMetadata();
metadata.setFlagsmithId(featureState.getFeature().getId());
return MapperFactory.getMapper().convertValue(metadata,
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {
});
}
/**
* Maps a segment to segment context.
*
* @param segment the segment JSON
* @return the segment context
*/
private static SegmentContext mapSegmentToSegmentContext(SegmentModel segment) {
// Map rules
List<SegmentRule> rules = mapEnvironmentDocumentRulesToContextRules(
segment.getRules());
// Map overrides
List<FeatureStateModel> segmentFeatureStates = segment.getFeatureStates();
List<FeatureContext> overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts(
segmentFeatureStates);
// Map metadata
SegmentMetadata metadata = new SegmentMetadata();
metadata.setSource(SegmentMetadata.Source.API);
metadata.setFlagsmithId(segment.getId());
Map<String, Object> metadataMap = mapSegmentMetadataToMetadataMap(metadata);
String segmentKey = String.valueOf(segment.getId());
return new SegmentContext()
.withKey(segmentKey)
.withName(segment.getName())
.withRules(rules)
.withOverrides(overrides)
.withMetadata(metadataMap);
}
/**
* Generates a unique segment key based on feature contexts.
* Uses a combination of feature names and values to ensure
* uniqueness.
*
* @param featureContexts list of feature contexts
* @return unique segment key
*/
private static String getVirtualSegmentKey(
List<FeatureContext> featureContexts) {
StringBuilder keyBuilder = new StringBuilder();
// Add feature information to the key
for (FeatureContext featureContext : featureContexts) {
keyBuilder.append(featureContext.getName())
.append(":")
.append(featureContext.getEnabled())
.append(":")
.append(featureContext.getValue())
.append("|");
}
// Generate a hash of the combined string for a shorter key
// This is safer than using List.hashCode() as we control the string content
return String.valueOf(keyBuilder.toString().hashCode());
}
private static Map<String, Object> mapSegmentMetadataToMetadataMap(SegmentMetadata metadata) {
return MapperFactory.getMapper().convertValue(metadata,
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {
});
}
}