sortedVariants = new ArrayList<>(variants);
+ sortedVariants.sort((a, b) -> {
+ Double priority = a.getPriority();
+ Double comparedPriority = b.getPriority();
+ return priority.compareTo(comparedPriority);
+ });
+ for (FeatureValue variant : sortedVariants) {
+ Double weight = variant.getWeight();
+ Float limit = startPercentage + weight.floatValue();
+ if (startPercentage <= percentageValue && percentageValue < limit) {
+ return new FlagResult().withEnabled(featureContext.getEnabled())
+ .withFeatureKey(featureContext.getFeatureKey())
+ .withName(featureContext.getName())
+ .withValue(variant.getValue())
+ .withReason("SPLIT; weight=" + weight.intValue())
+ .withMetadata(featureContext.getMetadata());
+ }
+ startPercentage = limit;
+ }
}
}
- return featureStates;
+ return new FlagResult().withEnabled(featureContext.getEnabled())
+ .withFeatureKey(featureContext.getFeatureKey())
+ .withName(featureContext.getName())
+ .withValue(featureContext.getValue())
+ .withReason("DEFAULT")
+ .withMetadata(featureContext.getMetadata());
}
}
\ No newline at end of file
diff --git a/src/main/java/com/flagsmith/flagengine/EngineConstants.java b/src/main/java/com/flagsmith/flagengine/EngineConstants.java
new file mode 100644
index 00000000..cd172485
--- /dev/null
+++ b/src/main/java/com/flagsmith/flagengine/EngineConstants.java
@@ -0,0 +1,6 @@
+package com.flagsmith.flagengine;
+
+public class EngineConstants {
+ public static final double STRONGEST_PRIORITY = Double.NEGATIVE_INFINITY;
+ public static final double WEAKEST_PRIORITY = Double.POSITIVE_INFINITY;
+}
diff --git a/src/main/java/com/flagsmith/flagengine/IdentityContext.java b/src/main/java/com/flagsmith/flagengine/IdentityContext.java
new file mode 100644
index 00000000..62329f3c
--- /dev/null
+++ b/src/main/java/com/flagsmith/flagengine/IdentityContext.java
@@ -0,0 +1,310 @@
+/*
+ * IdentityContext
+ *
+ * This class was auto-generated by jsonschema2pojo.org
+ * and is referenced in the Evaluation Context schema as
+ * `"existingJavaType": "com.flagsmith.flagengine.IdentityContext"`
+ * to work around jsonschema2pojo's lack of `anyOf` support.
+ */
+
+package com.flagsmith.flagengine;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * IdentityContext
+ *
+ *
Represents an identity context for feature flag evaluation.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({
+ "identifier",
+ "key",
+ "traits"
+})
+public class IdentityContext {
+
+ /**
+ * Identifier
+ *
+ *
A unique identifier for an identity, used for segment and multivariate
+ * feature flag targeting, and displayed in the Flagsmith UI.
+ * (Required)
+ */
+ @JsonProperty("identifier")
+ @JsonPropertyDescription("A unique identifier for an identity, used for segment and multivariate "
+ + "feature flag targeting, and displayed in the Flagsmith UI.")
+ private String identifier;
+
+ /**
+ * Key
+ *
+ *
Key used when selecting a value for a multivariate feature, or for % split
+ * segmentation. Set to an internal identifier or a composite value based on the
+ * environment key and identifier, depending on Flagsmith implementation.
+ * (Required)
+ */
+ @JsonProperty("key")
+ @JsonPropertyDescription("Key used when selecting a value for a multivariate feature, "
+ + "or for % split segmentation. Set to an internal identifier or a composite value "
+ + "based on the environment key and identifier, depending on Flagsmith implementation.")
+ private String key;
+
+ /**
+ * Traits
+ *
+ *
A map of traits associated with the identity, where the key is the trait name
+ * and the value is the trait value.
+ */
+ @JsonProperty("traits")
+ @JsonPropertyDescription("A map of traits associated with the identity, "
+ + "where the key is the trait name and the value is the trait value.")
+ private Traits traits;
+
+ @JsonIgnore
+ private Map additionalProperties = new LinkedHashMap();
+
+ /**
+ * No args constructor for use in serialization.
+ */
+ public IdentityContext() {
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param source the object being copied
+ */
+ public IdentityContext(IdentityContext source) {
+ super();
+ this.identifier = source.identifier;
+ this.key = source.key;
+ this.traits = source.traits;
+ }
+
+ /**
+ * Constructor with required fields.
+ *
+ * @param identifier A unique identifier for an identity
+ * @param key Key used when selecting a value for a multivariate feature
+ */
+ public IdentityContext(String identifier, String key) {
+ this.identifier = identifier;
+ this.key = key;
+ }
+
+ /**
+ * Constructor with all fields.
+ *
+ * @param identifier A unique identifier for an identity
+ * @param key Key used when selecting a value for a multivariate feature
+ * @param traits A map of traits associated with the identity
+ */
+ public IdentityContext(String identifier, String key, Traits traits) {
+ this.identifier = identifier;
+ this.key = key;
+ this.traits = traits;
+ }
+
+ /**
+ * Identifier
+ *
+ * A unique identifier for an identity, used for segment and multivariate
+ * feature flag targeting, and displayed in the Flagsmith UI.
+ * (Required)
+ */
+ @JsonProperty("identifier")
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ /**
+ * Identifier
+ *
+ *
A unique identifier for an identity, used for segment and multivariate
+ * feature flag targeting, and displayed in the Flagsmith UI.
+ * (Required)
+ */
+ @JsonProperty("identifier")
+ public void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ /**
+ * Fluent setter for identifier.
+ *
+ * @param identifier A unique identifier for an identity
+ * @return the IdentityContext instance
+ */
+ public IdentityContext withIdentifier(String identifier) {
+ this.identifier = identifier;
+ return this;
+ }
+
+ /**
+ * Key
+ *
+ *
Key used when selecting a value for a multivariate feature, or for % split
+ * segmentation. Set to an internal identifier or a composite value based on the
+ * environment key and identifier, depending on Flagsmith implementation.
+ * (Required)
+ */
+ @JsonProperty("key")
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Key
+ *
+ *
Key used when selecting a value for a multivariate feature, or for % split
+ * segmentation. Set to an internal identifier or a composite value based on the
+ * environment key and identifier, depending on Flagsmith implementation.
+ * (Required)
+ */
+ @JsonProperty("key")
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ /**
+ * Fluent setter for key.
+ *
+ * @param key the key
+ * @return the IdentityContext instance
+ */
+ public IdentityContext withKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Traits
+ *
+ *
A map of traits associated with the identity, where the key is the trait name
+ * and the value is the trait value.
+ */
+ @JsonProperty("traits")
+ public Traits getTraits() {
+ return traits;
+ }
+
+ /**
+ * Traits
+ *
+ *
A map of traits associated with the identity, where the key is the trait name
+ * and the value is the trait value.
+ */
+ @JsonProperty("traits")
+ public void setTraits(Traits traits) {
+ this.traits = traits;
+ }
+
+ /**
+ * Fluent setter for traits.
+ *
+ * @param traits A map of traits associated with the identity
+ * @return the IdentityContext instance
+ */
+ public IdentityContext withTraits(Traits traits) {
+ this.traits = traits;
+ return this;
+ }
+
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return this.additionalProperties;
+ }
+
+ /**
+ * Set additional property.
+ *
+ * @param name the name
+ * @param value the value
+ */
+ @JsonAnySetter
+ public void setAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ }
+
+ /**
+ * Fluent setter for additional properties.
+ *
+ * @param name the name of the additional property
+ * @param value the value of the additional property
+ * @return the IdentityContext instance
+ */
+ public IdentityContext withAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(IdentityContext.class.getName()).append('@')
+ .append(Integer.toHexString(System.identityHashCode(this))).append('[');
+ sb.append("identifier");
+ sb.append('=');
+ sb.append(((this.identifier == null) ? "" : this.identifier));
+ sb.append(',');
+ sb.append("key");
+ sb.append('=');
+ sb.append(((this.key == null) ? "" : this.key));
+ sb.append(',');
+ sb.append("traits");
+ sb.append('=');
+ sb.append(((this.traits == null) ? "" : this.traits));
+ sb.append(',');
+ sb.append("additionalProperties");
+ sb.append('=');
+ sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties));
+ sb.append(',');
+ if (sb.charAt((sb.length() - 1)) == ',') {
+ sb.setCharAt((sb.length() - 1), ']');
+ } else {
+ sb.append(']');
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = ((result * 31) + ((this.identifier == null) ? 0 : this.identifier.hashCode()));
+ result = ((result * 31) + ((this.traits == null) ? 0 : this.traits.hashCode()));
+ result = ((result * 31) + ((this.additionalProperties == null) ? 0
+ : this.additionalProperties.hashCode()));
+ result = ((result * 31) + ((this.key == null) ? 0 : this.key.hashCode()));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if ((other instanceof IdentityContext) == false) {
+ return false;
+ }
+ IdentityContext rhs = ((IdentityContext) other);
+ return (((((this.identifier == rhs.identifier)
+ || ((this.identifier != null) && this.identifier.equals(rhs.identifier)))
+ && ((this.traits == rhs.traits) || ((this.traits != null)
+ && this.traits.equals(rhs.traits))))
+ && ((this.additionalProperties == rhs.additionalProperties)
+ || ((this.additionalProperties != null)
+ && this.additionalProperties.equals(rhs.additionalProperties))))
+ && ((this.key == rhs.key) || ((this.key != null)
+ && this.key.equals(rhs.key))));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java
new file mode 100644
index 00000000..892a5c80
--- /dev/null
+++ b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java
@@ -0,0 +1,154 @@
+package com.flagsmith.flagengine;
+
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.flagsmith.flagengine.segments.constants.SegmentConditions;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import lombok.Data;
+
+@Data
+public class SegmentCondition {
+ private SegmentConditions operator;
+ private Object value;
+ private String property;
+
+ /**
+ * No args constructor for use in serialization.
+ */
+ public SegmentCondition() {
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param source the object being copied
+ */
+ public SegmentCondition(SegmentCondition source) {
+ super();
+ this.operator = source.operator;
+ this.value = source.value;
+ this.property = source.property;
+ }
+
+ /**
+ * Constructor with all fields.
+ *
+ * @param operator the segment condition operator
+ * @param property the property name
+ * @param value the condition value
+ */
+ public SegmentCondition(SegmentConditions operator, String property, Object value) {
+ this.operator = operator;
+ this.property = property;
+ this.value = value;
+ }
+
+ /**
+ * Set JsonNode value. Required for engine tests.
+ *
+ * @param node the JsonNode value.
+ * @throws IllegalArgumentException if value is not a String or List of Strings
+ */
+ @JsonSetter("value")
+ public void setValue(JsonNode node) {
+ if (node.isArray()) {
+ ArrayNode arr = (ArrayNode) node;
+ List values = StreamSupport.stream(arr.spliterator(), false)
+ .peek(el -> {
+ if (!el.isTextual()) {
+ throw new IllegalArgumentException("Array elements must be strings");
+ }
+ })
+ .map(JsonNode::asText)
+ .collect(Collectors.toList());
+ this.setValue(values);
+ } else if (node.isTextual()) {
+ this.setValue(node.asText());
+ } else {
+ throw new IllegalArgumentException("Value must be a String or List of Strings");
+ }
+ }
+
+ /**
+ * Set String value.
+ *
+ * @param value New String value.
+ */
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Set List value.
+ *
+ * @param value New List value.
+ */
+ public void setValue(List value) {
+ if (this.operator != SegmentConditions.IN) {
+ throw new IllegalArgumentException("List value can only be set for IN operator");
+ }
+ this.value = value;
+ }
+
+ /**
+ * Fluent setter for operator.
+ *
+ * @param operator the segment condition operator
+ * @return the SegmentCondition instance
+ */
+ public SegmentCondition withOperator(SegmentConditions operator) {
+ this.operator = operator;
+ return this;
+ }
+
+ /**
+ * Fluent setter for property.
+ *
+ * @param property the property name
+ * @return the SegmentCondition instance
+ */
+ public SegmentCondition withProperty(String property) {
+ this.property = property;
+ return this;
+ }
+
+ /**
+ * Fluent setter for value.
+ *
+ * @param value the condition value
+ * @return the SegmentCondition instance
+ */
+ public SegmentCondition withValue(Object value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * Fluent setter for String value.
+ *
+ * @param value the String condition value
+ * @return the SegmentCondition instance
+ */
+ public SegmentCondition withValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * Fluent setter for List value.
+ *
+ * @param value the List condition value
+ * @return the SegmentCondition instance
+ * @throws IllegalArgumentException if operator is not IN
+ */
+ public SegmentCondition withValue(List value) {
+ if (this.operator != SegmentConditions.IN) {
+ throw new IllegalArgumentException("List value can only be set for IN operator");
+ }
+ this.value = value;
+ return this;
+ }
+}
diff --git a/src/main/java/com/flagsmith/flagengine/Traits.java b/src/main/java/com/flagsmith/flagengine/Traits.java
new file mode 100644
index 00000000..3cf83229
--- /dev/null
+++ b/src/main/java/com/flagsmith/flagengine/Traits.java
@@ -0,0 +1,109 @@
+package com.flagsmith.flagengine;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Traits
+ *
+ * A map of traits associated with the identity, where the key is the trait name
+ * and the value is the trait value.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({
+
+})
+public class Traits {
+
+ @JsonIgnore
+ private Map additionalProperties = new LinkedHashMap();
+
+ /**
+ * No args constructor for use in serialization.
+ */
+ public Traits() {
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param source the object being copied
+ */
+ public Traits(Traits source) {
+ super();
+ this.additionalProperties = new LinkedHashMap<>(source.additionalProperties);
+ }
+
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return this.additionalProperties;
+ }
+
+ /**
+ * Set additional property.
+ *
+ * @param name the name
+ * @param value the value
+ */
+ @JsonAnySetter
+ public void setAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ }
+
+ /**
+ * Fluent setter for additional property.
+ *
+ * @param name the name
+ * @param value the value
+ * @return the Traits instance
+ */
+ public Traits withAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Traits.class.getName()).append('@')
+ .append(Integer.toHexString(System.identityHashCode(this))).append('[');
+ sb.append("additionalProperties");
+ sb.append('=');
+ sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties));
+ sb.append(',');
+ if (sb.charAt((sb.length() - 1)) == ',') {
+ sb.setCharAt((sb.length() - 1), ']');
+ } else {
+ sb.append(']');
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = ((result * 31) + ((this.additionalProperties == null) ? 0
+ : this.additionalProperties.hashCode()));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if ((other instanceof Traits) == false) {
+ return false;
+ }
+ Traits rhs = ((Traits) other);
+ return ((this.additionalProperties == rhs.additionalProperties)
+ || ((this.additionalProperties != null)
+ && this.additionalProperties.equals(rhs.additionalProperties)));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java b/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java
deleted file mode 100644
index c210771a..00000000
--- a/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.flagsmith.flagengine.environments.integrations;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-
-@Data
-public class IntegrationModel {
- @JsonProperty("api_key")
- private String apiKey;
- @JsonProperty("base_url")
- private String baseUrl;
-}
diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java
deleted file mode 100644
index 4b6a7cdd..00000000
--- a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.flagsmith.flagengine.features;
-
-import lombok.Data;
-
-@Data
-public class FeatureModel {
- private Integer id;
- private String name;
- private String type;
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof FeatureModel)) {
- return false;
- }
-
- return id != null && id.equals(((FeatureModel) o).getId());
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java
deleted file mode 100644
index 5118dd91..00000000
--- a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.flagsmith.flagengine.features;
-
-import com.flagsmith.flagengine.utils.models.BaseModel;
-import lombok.Data;
-
-@Data
-public class FeatureSegmentModel extends BaseModel {
- private Integer priority;
-
- public FeatureSegmentModel() {
- this.priority = 0;
- }
-
- public FeatureSegmentModel(Integer priority) {
- this.priority = priority;
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java
deleted file mode 100644
index 1cec6f17..00000000
--- a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.flagsmith.flagengine.features;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.flagsmith.flagengine.utils.Hashing;
-import com.flagsmith.utils.models.BaseModel;
-import java.util.Arrays;
-import java.util.List;
-import java.util.UUID;
-import java.util.stream.Collectors;
-import lombok.Data;
-
-@Data
-public class FeatureStateModel extends BaseModel {
- private FeatureModel feature;
- private Boolean enabled;
- @JsonProperty("django_id")
- private Integer djangoId;
- @JsonProperty("featurestate_uuid")
- private String featurestateUuid = UUID.randomUUID().toString();
- @JsonProperty("multivariate_feature_state_values")
- private List multivariateFeatureStateValues;
- @JsonProperty("feature_state_value")
- private Object value;
- @JsonProperty("feature_segment")
- private FeatureSegmentModel featureSegment;
-
- /**
- * Returns the value object.
- *
- * @param identityId Identity ID
- */
- public Object getValue(Object identityId) {
-
- if (identityId != null && multivariateFeatureStateValues != null
- && multivariateFeatureStateValues.size() > 0) {
- return getMultiVariateValue(identityId);
- }
-
- return value;
- }
-
- /**
- * Determines the multi variate value.
- *
- * @param identityId Identity ID
- */
- private Object getMultiVariateValue(Object identityId) {
-
- List objectIds = Arrays.asList(
- (djangoId != null && djangoId != 0 ? djangoId.toString() : featurestateUuid),
- identityId.toString()
- );
-
- Float percentageValue = Hashing.getInstance().getHashedPercentageForObjectIds(objectIds);
- Float startPercentage = 0f;
-
- List sortedMultiVariateFeatureStates =
- multivariateFeatureStateValues
- .stream()
- .sorted((smvfs1, smvfs2) -> smvfs1.getSortValue().compareTo(smvfs2.getSortValue()))
- .collect(Collectors.toList());
-
- for (MultivariateFeatureStateValueModel multiVariate : sortedMultiVariateFeatureStates) {
- Float limit = multiVariate.getPercentageAllocation() + startPercentage;
-
- if (startPercentage <= percentageValue && percentageValue < limit) {
- return multiVariate.getMultivariateFeatureOption().getValue();
- }
-
- startPercentage = limit;
- }
-
- return value;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof FeatureStateModel)) {
- return false;
- }
-
- return this.getFeature().getId() == ((FeatureStateModel) o).getFeature().getId();
- }
-
- /**
- * Another FeatureStateModel is deemed to be higher priority if and only if
- * it has a FeatureSegment and either this.FeatureSegment is null or the
- * value of other.FeatureSegment.priority is lower than that of
- * this.FeatureSegment.priority.
- *
- * @param other the other FeatureStateModel to compare priority with
- * @return true if `this` is higher priority than `other`
- */
- public boolean isHigherPriority(FeatureStateModel other) {
- if (this.featureSegment == null || other.featureSegment == null) {
- return this.featureSegment != null && other.featureSegment == null;
- }
-
- return this.featureSegment.getPriority() < other.featureSegment.getPriority();
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java
deleted file mode 100644
index d7aa5902..00000000
--- a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.flagsmith.flagengine.identities;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.flagsmith.flagengine.features.FeatureStateModel;
-import com.flagsmith.flagengine.identities.traits.TraitModel;
-import com.flagsmith.utils.models.BaseModel;
-import java.sql.Date;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.stream.Collectors;
-import lombok.Data;
-
-@Data
-public class IdentityModel extends BaseModel {
- @JsonProperty("django_id")
- private Integer djangoId;
- private String identifier;
- @JsonProperty("environment_api_key")
- private String environmentApiKey;
- @JsonProperty("created_date")
- private Date createdDate;
- @JsonProperty("identity_uuid")
- private String identityUuid = UUID.randomUUID().toString();
- @JsonProperty("identity_traits")
- private List extends TraitModel> identityTraits = new ArrayList<>();
- @JsonProperty("identity_features")
- private List identityFeatures = new ArrayList<>();
- @JsonProperty("composite_key")
- private String compositeKey;
-
- /**
- * Returns the composite key for the identity.
- */
- public String getCompositeKey() {
- if (compositeKey == null) {
- compositeKey = environmentApiKey + "_" + identifier;
- }
- return compositeKey;
- }
-
- /**
- * Update the identity traits.
- *
- * @param traits traits to update
- */
- public void updateTraits(List extends TraitModel> traits) {
- Map existingTraits = new HashMap<>();
-
- if (identityTraits != null && identityTraits.size() > 0) {
- existingTraits = identityTraits.stream()
- .collect(Collectors.toMap(TraitModel::getTraitKey, (trait) -> trait));
- }
-
- for (TraitModel trait : traits) {
- if (trait.getTraitValue() == null) {
- existingTraits.remove(trait.getTraitKey());
- } else {
- existingTraits.put(trait.getTraitKey(), trait);
- }
- }
-
- identityTraits = existingTraits.values()
- .stream().collect(Collectors.toList());
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java b/src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java
deleted file mode 100644
index eaacda9b..00000000
--- a/src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.flagsmith.flagengine.organisations;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.flagsmith.utils.models.BaseModel;
-import lombok.Data;
-
-@Data
-public class OrganisationModel extends BaseModel {
- private Integer id;
- private String name;
- @JsonProperty("feature_analytics")
- private Boolean featureAnalytics;
- @JsonProperty("stop_serving_flags")
- private Boolean stopServingFlags;
- @JsonProperty("persist_trait_data")
- private Boolean persistTraitData;
-
- public String uniqueSlug() {
- return id.toString() + "-" + name;
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java b/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java
deleted file mode 100644
index 2e00dd31..00000000
--- a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.flagsmith.flagengine.projects;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.flagsmith.flagengine.organisations.OrganisationModel;
-import com.flagsmith.flagengine.segments.SegmentModel;
-import com.flagsmith.utils.models.BaseModel;
-import java.util.List;
-import lombok.Data;
-
-@Data
-public class ProjectModel extends BaseModel {
- private Integer id;
- private String name;
- @JsonProperty("hide_disabled_flags")
- private Boolean hideDisabledFlags;
- private OrganisationModel organisation;
- private List segments;
-}
diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java
index c0b2856e..b98dd5db 100644
--- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java
+++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java
@@ -1,196 +1,212 @@
package com.flagsmith.flagengine.segments;
-import com.flagsmith.flagengine.environments.EnvironmentModel;
-import com.flagsmith.flagengine.identities.IdentityModel;
-import com.flagsmith.flagengine.identities.traits.TraitModel;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.flagsmith.flagengine.EvaluationContext;
+import com.flagsmith.flagengine.SegmentCondition;
+import com.flagsmith.flagengine.SegmentContext;
+import com.flagsmith.flagengine.SegmentRule;
import com.flagsmith.flagengine.segments.constants.SegmentConditions;
import com.flagsmith.flagengine.utils.Hashing;
import com.flagsmith.flagengine.utils.types.TypeCasting;
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.Option;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Optional;
+import java.util.Map;
+import java.util.function.Predicate;
import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
public class SegmentEvaluator {
+ private static ObjectMapper mapper = new ObjectMapper();
+ private static Configuration jsonPathConfiguration = Configuration
+ .defaultConfiguration()
+ .setOptions(Option.SUPPRESS_EXCEPTIONS);
+ private static TypeReference> stringListTypeRef = new TypeReference>() {
+ };
/**
- * Get segment identities from environment and identity.
+ * Check if context is in segment.
*
- * @param environment Environment instance.
- * @param identity Identity Instance.
+ * @param context Evaluation context.
+ * @param segment Segment context.
+ * @return true if context is in segment.
*/
- public static List getIdentitySegments(EnvironmentModel environment,
- IdentityModel identity) {
- return getIdentitySegments(environment, identity, null);
+ public static Boolean isContextInSegment(EvaluationContext context, SegmentContext segment) {
+ List rules = segment.getRules();
+ return !rules.isEmpty() && rules.stream()
+ .allMatch((rule) -> contextMatchesRule(context, rule, segment.getKey()));
}
- /**
- * Get segment identities from environment and identity along with traits to override.
- *
- * @param environment Environment Instance.
- * @param identity Identity Instance.
- * @param overrideTraits Traits to over ride.
- */
- public static List getIdentitySegments(EnvironmentModel environment,
- IdentityModel identity,
- List overrideTraits) {
- return environment
- .getProject()
- .getSegments()
- .stream()
- .filter((segment) -> evaluateIdentityInSegment(identity, segment, overrideTraits))
- .collect(Collectors.toList());
- }
-
- /**
- * Evaluate the traits in identities and overrides with rules from segments.
- *
- * @param identity Identity instance.
- * @param segment Segment Instance.
- * @param overrideTraits Overriden traits.
- */
- public static Boolean evaluateIdentityInSegment(IdentityModel identity, SegmentModel segment,
- List extends TraitModel> overrideTraits) {
- List segmentRules = segment.getRules();
- List extends TraitModel> traits =
- overrideTraits != null ? overrideTraits : identity.getIdentityTraits();
-
- String identityHashKey = identity.getDjangoId() == null ? identity.getCompositeKey()
- : identity.getDjangoId().toString();
-
- if (segmentRules != null && segmentRules.size() > 0) {
- List segmentRuleEvaluations = segmentRules.stream().map(
- (rule) -> traitsMatchSegmentRule(
- traits,
- rule,
- segment.getId(),
- identityHashKey
- )
- ).collect(Collectors.toList());
-
- return segmentRuleEvaluations.stream().allMatch((bool) -> bool);
+ private static Boolean contextMatchesRule(EvaluationContext context, SegmentRule rule,
+ String segmentKey) {
+ Predicate conditionPredicate = (condition) -> contextMatchesCondition(
+ context, condition, segmentKey);
+
+ Boolean isMatch;
+ List conditions = rule.getConditions();
+
+ if (conditions.isEmpty()) {
+ isMatch = true;
+ } else {
+ switch (rule.getType()) {
+ case ALL:
+ isMatch = conditions.stream().allMatch(conditionPredicate);
+ break;
+ case ANY:
+ isMatch = conditions.stream().anyMatch(conditionPredicate);
+ break;
+ case NONE:
+ isMatch = conditions.stream().noneMatch(conditionPredicate);
+ break;
+ default:
+ return false;
+ }
}
- return Boolean.FALSE;
+ return isMatch && rule.getRules().stream()
+ .allMatch((subRule) -> contextMatchesRule(context, subRule, segmentKey));
}
- /**
- * Evaluate whether the trait match the rule from segment.
- *
- * @param identityTraits Traits to match against.
- * @param rule Rule from segments to evaluate with.
- * @param segmentId Segment ID (for hashing)
- * @param identityId Identity ID (for hashing)
- */
- private static Boolean traitsMatchSegmentRule(List extends TraitModel> identityTraits,
- SegmentRuleModel rule,
- Integer segmentId, String identityId) {
- Boolean matchingCondition = Boolean.TRUE;
-
- if (rule.getConditions() != null && rule.getConditions().size() > 0) {
- List conditionEvaluations = rule.getConditions().stream()
- .map((condition) -> traitsMatchSegmentCondition(identityTraits, condition, segmentId,
- identityId))
- .collect(Collectors.toList());
-
- matchingCondition = rule.matchingFunction(
- conditionEvaluations.stream()
- );
- }
+ private static Boolean contextMatchesCondition(
+ EvaluationContext context,
+ SegmentCondition condition,
+ String segmentKey) {
+ Object contextValue = getContextValue(context, condition.getProperty());
+ Object conditionValue = condition.getValue();
+ SegmentConditions operator = condition.getOperator();
- List rules = rule.getRules();
+ switch (operator) {
+ case IN:
+ List conditionList = new ArrayList<>();
+
+ if (conditionValue instanceof List) {
+ List> maybeConditionList = (List>) conditionValue;
+ conditionList = maybeConditionList.stream()
+ .filter(String.class::isInstance)
+ .map(Object::toString)
+ .collect(Collectors.toList());
+ } else if (conditionValue instanceof String) {
+ String stringConditionValue = (String) conditionValue;
+ try {
+ // Try parsing a JSON list first
+ conditionList = mapper.readValue(
+ stringConditionValue, stringListTypeRef);
+ } catch (IOException e) {
+ // As a fallback, split by comma
+ conditionList = Arrays.asList(stringConditionValue.split(","));
+ }
+ }
- if (rules != null) {
- matchingCondition = matchingCondition && rules.stream()
- .allMatch((segmentRule) -> traitsMatchSegmentRule(
- identityTraits,
- segmentRule,
- segmentId,
- identityId
- ));
- }
+ if (!(contextValue instanceof Boolean) && contextValue != null) {
+ contextValue = String.valueOf(contextValue);
+ }
- return matchingCondition;
- }
+ return conditionList.contains(contextValue);
+
+ case PERCENTAGE_SPLIT:
+ String key;
+ if (contextValue == null) {
+ if (context.getIdentity() == null) {
+ return false;
+ }
+ key = context.getIdentity().getKey();
+ } else {
+ key = contextValue.toString();
+ }
+ List objectIds = List.of(segmentKey, key);
- /**
- * Evaluate traits and compare them with condition.
- *
- * @param identityTraits Traits to match against.
- * @param condition Condition to evaluate with.
- * @param segmentId Segment ID (for hashing)
- * @param identityId Identity ID (for hashing)
- */
- private static Boolean traitsMatchSegmentCondition(List extends TraitModel> identityTraits,
- SegmentConditionModel condition,
- Integer segmentId, String identityId) {
- if (condition.getOperator().equals(SegmentConditions.PERCENTAGE_SPLIT)) {
- try {
- Float floatValue = Float.parseFloat(condition.getValue());
- return Hashing.getInstance().getHashedPercentageForObjectIds(
- Arrays.asList(segmentId.toString(), identityId)) <= floatValue;
-
- } catch (NumberFormatException nfe) {
- return Boolean.FALSE;
- }
- }
+ final float floatValue;
+ try {
+ floatValue = Float.parseFloat(String.valueOf(condition.getValue()));
+ } catch (NumberFormatException e) {
+ return false;
+ }
- if (identityTraits != null) {
- Optional extends TraitModel> matchingTrait = identityTraits
- .stream()
- .filter((trait) -> trait.getTraitKey().equals(condition.getProperty_()))
- .findFirst();
+ return Hashing.getInstance()
+ .getHashedPercentageForObjectIds(objectIds) <= floatValue;
- return traitMatchesSegmentCondition(matchingTrait, condition);
- }
+ case IS_NOT_SET:
+ return contextValue == null;
- return condition.getOperator().equals(SegmentConditions.IS_NOT_SET);
- }
+ case IS_SET:
+ return contextValue != null;
- /**
- * Evaluate a single trait and compare it with condition.
- *
- * @param trait Trait to match against.
- * @param condition Condition to evaluate with.
- */
- private static Boolean traitMatchesSegmentCondition(Optional extends TraitModel> trait,
- SegmentConditionModel condition) {
- if (condition.getOperator().equals(SegmentConditions.IS_NOT_SET)) {
- return !trait.isPresent();
- } else if (condition.getOperator().equals(SegmentConditions.IS_SET)) {
- return trait.isPresent();
- }
-
- return trait.isPresent() && conditionMatchesTraitValue(condition, trait.get().getTraitValue());
- }
+ case CONTAINS:
+ return (String.valueOf(contextValue)).indexOf(conditionValue.toString()) > -1;
- /**
- * Matches condition value with the trait value.
- *
- * @param condition Condition to evaluate with.
- * @param value Trait value to compare with.
- */
- public static Boolean conditionMatchesTraitValue(SegmentConditionModel condition, Object value) {
- SegmentConditions operator = condition.getOperator();
- switch (operator) {
case NOT_CONTAINS:
- return (String.valueOf(value)).indexOf(condition.getValue()) == -1;
- case CONTAINS:
- return (String.valueOf(value)).indexOf(condition.getValue()) > -1;
- case IN:
- if (value instanceof String) {
- return Arrays.asList(condition.getValue().split(",")).contains(value);
- }
- if (value instanceof Integer) {
- return Arrays.asList(condition.getValue().split(",")).contains(String.valueOf(value));
+ if (contextValue != null) {
+ return (String.valueOf(contextValue)).indexOf(conditionValue.toString()) == -1;
}
return false;
+
case REGEX:
- Pattern pattern = Pattern.compile(condition.getValue());
- return pattern.matcher(String.valueOf(value)).find();
+ if (contextValue != null) {
+ try {
+ Pattern pattern = Pattern.compile(conditionValue.toString());
+ return pattern.matcher(contextValue.toString()).find();
+ } catch (PatternSyntaxException pse) {
+ return false;
+ }
+ }
+ return false;
+
+ case MODULO:
+ if (contextValue instanceof Number && conditionValue instanceof String) {
+ try {
+ String[] parts = conditionValue.toString().split("\\|");
+ if (parts.length != 2) {
+ return false;
+ }
+ Double divisor = Double.parseDouble(parts[0]);
+ Double remainder = Double.parseDouble(parts[1]);
+ Double value = ((Number) contextValue).doubleValue();
+ return (value % divisor) == remainder;
+ } catch (NumberFormatException nfe) {
+ return false;
+ }
+ }
+ return false;
+
default:
- return TypeCasting.compare(operator, value, condition.getValue());
+ if (contextValue == null) {
+ return false;
+ }
+ return TypeCasting.compare(operator, contextValue, conditionValue);
+ }
+ }
+
+ /**
+ * Get context value by property name.
+ *
+ * @param context Evaluation context.
+ * @param property Property name.
+ * @return Property value.
+ */
+ private static Object getContextValue(EvaluationContext context, String property) {
+ Object result;
+ if (context.getIdentity() != null && context.getIdentity().getTraits() != null) {
+ result = context.getIdentity().getTraits().getAdditionalProperties().get(property);
+ if (result != null) {
+ return result;
+ }
+ }
+ if (property.startsWith("$.")) {
+ result = JsonPath
+ .using(jsonPathConfiguration)
+ .parse(mapper.convertValue(context, Map.class))
+ .read(property);
+ if (result instanceof List || result instanceof Map) {
+ return null;
+ }
+ return result;
}
+ return null;
}
}
\ No newline at end of file
diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java
deleted file mode 100644
index 39a6d0d6..00000000
--- a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.flagsmith.flagengine.segments;
-
-import com.flagsmith.flagengine.segments.constants.SegmentRules;
-import java.util.List;
-import java.util.stream.Stream;
-import lombok.Data;
-
-@Data
-public class SegmentRuleModel {
- private String type;
- private List rules;
- private List conditions;
-
- /**
- * Run the matching function against the boolean stream.
- *
- * @param booleanStream Boolean stream from trait condition evaluations.
- */
- public Boolean matchingFunction(Stream booleanStream) {
- if (SegmentRules.ALL_RULE.getRule().equals(type)) {
- return booleanStream.allMatch((bool) -> bool);
- } else if (SegmentRules.ANY_RULE.getRule().equals(type)) {
- return booleanStream.anyMatch((bool) -> bool);
- } else if (SegmentRules.NONE_RULE.getRule().equals(type)) {
- return !booleanStream.anyMatch((bool) -> bool);
- }
-
- return false;
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java b/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java
deleted file mode 100644
index 683cf1ec..00000000
--- a/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.flagsmith.flagengine.segments.constants;
-
-public enum SegmentRules {
- ALL_RULE("ALL"), ANY_RULE("ANY"), NONE_RULE("NONE");
-
- private String rule;
-
- public String getRule() {
- return rule;
- }
-
- private SegmentRules(String rule) {
- this.rule = rule;
- }
-}
diff --git a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java
index 3ea6d62e..9b05b2f3 100644
--- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java
+++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java
@@ -32,7 +32,7 @@ public static Boolean compare(SegmentConditions condition, Object value1, Object
return compare(condition, toSemver(value1), toSemver(value2));
}
- return compare(condition, (String) value1, (String) value2);
+ return compare(condition, String.valueOf(value1), String.valueOf(value2));
}
/**
@@ -140,7 +140,7 @@ public static Boolean toBoolean(Object str) {
return str instanceof Boolean ? ((Boolean) str)
: BooleanUtils.toBoolean((String) str);
} catch (Exception nfe) {
- return null;
+ return false;
}
}
@@ -151,8 +151,9 @@ public static Boolean toBoolean(Object str) {
*/
public static Boolean isBoolean(Object str) {
return str instanceof Boolean
- || Boolean.TRUE.toString().equalsIgnoreCase(((String) str))
- || Boolean.FALSE.toString().equalsIgnoreCase(((String) str));
+ || Boolean.TRUE.toString().equalsIgnoreCase((String.valueOf(str)))
+ || Boolean.FALSE.toString().equalsIgnoreCase((String.valueOf(str)))
+ || "1".equals(String.valueOf(str));
}
/**
diff --git a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java
index c3283d14..c8305786 100644
--- a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java
+++ b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java
@@ -1,9 +1,9 @@
package com.flagsmith.interfaces;
import com.flagsmith.config.FlagsmithConfig;
-import com.flagsmith.flagengine.environments.EnvironmentModel;
-import com.flagsmith.flagengine.identities.traits.TraitModel;
+import com.flagsmith.flagengine.EvaluationContext;
import com.flagsmith.models.Flags;
+import com.flagsmith.models.TraitModel;
import com.flagsmith.threads.RequestProcessor;
import java.util.List;
import okhttp3.HttpUrl;
@@ -21,7 +21,7 @@ Flags identifyUserWithTraits(
FlagsmithConfig getConfig();
- EnvironmentModel getEnvironment();
+ EvaluationContext getEvaluationContext();
RequestProcessor getRequestor();
diff --git a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java
index 48f6c1d9..ab574089 100644
--- a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java
+++ b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java
@@ -1,6 +1,6 @@
package com.flagsmith.interfaces;
-import com.flagsmith.flagengine.environments.EnvironmentModel;
+import com.flagsmith.models.environments.EnvironmentModel;
public interface IOfflineHandler {
EnvironmentModel getEnvironment();
diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java
new file mode 100644
index 00000000..26087c31
--- /dev/null
+++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java
@@ -0,0 +1,463 @@
+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.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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * EngineMappers
+ *
+ * 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);
+
+ if (metadata == null || metadata.getFlagsmithId() == null) {
+ return null;
+ }
+
+ 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 traits) {
+
+ // Create identity context
+ IdentityContext identityContext = new IdentityContext()
+ .withIdentifier(identifier)
+ .withKey(context.getEnvironment().getKey() + "_" + identifier)
+ .withTraits(new Traits());
+
+ // Map traits if provided
+ if (traits != null && !traits.isEmpty()) {
+ for (Map.Entry entry : traits.entrySet()) {
+ Object traitValue = entry.getValue();
+ // Handle TraitConfig-like objects (maps with "value" key)
+ if (traitValue instanceof Map) {
+ Map, ?> traitMap = (Map, ?>) traitValue;
+ if (traitMap.containsKey("value")) {
+ traitValue = traitMap.get("value");
+ }
+ }
+ identityContext.getTraits().setAdditionalProperty(entry.getKey(), traitValue);
+ }
+ }
+
+ // 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 features = new HashMap<>();
+ for (FeatureStateModel featureState : environmentModel.getFeatureStates()) {
+ FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState);
+ features.put(featureContext.getName(), featureContext);
+ }
+
+ // Map segments
+ Map 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 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 entry : features.entrySet()) {
+ featuresObj.withAdditionalProperty(entry.getKey(), entry.getValue());
+ }
+ evaluationContext.withFeatures(featuresObj);
+
+ // Add segments individually
+ Segments segmentsObj = new Segments();
+ for (Map.Entry 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 mapIdentityOverridesToSegments(
+ List identityOverrides) {
+
+ // Map from sorted list of feature contexts to identifiers
+ Map, List> featuresToIdentifiers = new HashMap<>();
+
+ for (IdentityModel identityOverride : identityOverrides) {
+ List identityFeatures = identityOverride.getIdentityFeatures();
+ if (identityFeatures == null || identityFeatures.isEmpty()) {
+ continue;
+ }
+
+ // Create overrides key as a sorted list of FeatureContext objects
+ List overridesKey = new ArrayList<>();
+ List 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("")
+ .withFeatureKey(String.valueOf(feature.getId()))
+ .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 segmentContexts = new HashMap<>();
+ for (Map.Entry, List> entry : featuresToIdentifiers.entrySet()) {
+ List overridesKey = entry.getKey();
+ List 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 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 metadataMap = MapperFactory.getMapper()
+ .convertValue(metadata,
+ new com.fasterxml.jackson.core.type.TypeReference