From 14bf7cc245c8761cbf8736e6b722938e867987e9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 10:53:02 +0100 Subject: [PATCH 01/62] wip --- pom.xml | 28 +- .../java/com/flagsmith/flagengine/Engine.java | 219 +++++-------- .../flagsmith/flagengine/IdentityContext.java | 301 ++++++++++++++++++ .../flagengine/SegmentCondition.java | 128 ++++++++ .../java/com/flagsmith/flagengine/Traits.java | 109 +++++++ .../environments/EnvironmentModel.java | 24 -- .../integrations/IntegrationModel.java | 12 - .../flagengine/features/FeatureModel.java | 24 -- .../features/FeatureSegmentModel.java | 17 - .../features/FeatureStateModel.java | 101 ------ .../MultivariateFeatureOptionModel.java | 10 - .../MultivariateFeatureStateValueModel.java | 21 -- .../flagengine/identities/IdentityModel.java | 70 ---- .../identities/traits/TraitModel.java | 18 -- .../organisations/OrganisationModel.java | 21 -- .../flagengine/projects/ProjectModel.java | 18 -- .../segments/SegmentConditionModel.java | 13 - .../flagengine/segments/SegmentEvaluator.java | 299 ++++++++--------- .../flagengine/segments/SegmentModel.java | 17 - .../flagengine/segments/SegmentRuleModel.java | 30 -- .../segments/constants/SegmentRules.java | 15 - .../flagsmith/interfaces/IOfflineHandler.java | 4 +- .../resources/schema/evaluation-context.json | 3 + .../resources/schema/evaluation-result.json | 3 + 24 files changed, 802 insertions(+), 703 deletions(-) create mode 100644 src/main/java/com/flagsmith/flagengine/IdentityContext.java create mode 100644 src/main/java/com/flagsmith/flagengine/SegmentCondition.java create mode 100644 src/main/java/com/flagsmith/flagengine/Traits.java delete mode 100644 src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java delete mode 100644 src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java create mode 100644 src/main/resources/schema/evaluation-context.json create mode 100644 src/main/resources/schema/evaluation-result.json diff --git a/pom.xml b/pom.xml index 0b892651..1b4cadc2 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,11 @@ okhttp 4.12.0 + + com.jayway.jsonpath + json-path + 2.9.0 + com.fasterxml.jackson.core jackson-annotations @@ -181,6 +186,27 @@ flagsmith-java-client-${project.version} + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + 1.2.2 + + + + generate + + + + + jsonschema + ${project.basedir}/src/main/resources/schema + com.flagsmith.flagengine + ${project.build.directory}/generated-sources/jsonschema2pojo + true + true + true + + org.sonatype.central central-publishing-maven-plugin @@ -316,4 +342,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index ec9beb46..c5191574 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -1,153 +1,110 @@ package com.flagsmith.flagengine; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; import com.flagsmith.flagengine.segments.SegmentEvaluator; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.flagengine.utils.exceptions.FeatureStateNotFound; +import com.flagsmith.flagengine.utils.Hashing; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.ImmutablePair; public class Engine { - /** - * Get a list of feature states for a given environment. + * Evaluate if identity is in segment. * - * @param environment Instance of the Environment. + * @param context Identity Instance. + * @return True adsa. */ - public static List getEnvironmentFeatureStates(EnvironmentModel environment) { - if (environment.getProject().getHideDisabledFlags()) { - return environment.getFeatureStates() - .stream() - .filter((featureState) -> featureState.getEnabled()) - .collect(Collectors.toList()); + public static EvaluationResult getEvaluationResult(EvaluationContext context) { + List segments = new ArrayList<>(); + HashMap> segmentFeatureContexts = new HashMap<>(); + List flags = new ArrayList<>(); + + for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { + if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { + SegmentResult segmentResult = new SegmentResult().withKey(segmentContext.getKey()) + .withName(segmentContext.getName()); + segments.add(segmentResult); + + if (segmentContext.getOverrides() != null) { + for (FeatureContext featureContext : segmentContext.getOverrides()) { + String featureKey = featureContext.getFeatureKey(); + + if (segmentFeatureContexts.containsKey(featureKey)) { + ImmutablePair existing = segmentFeatureContexts + .get(featureKey); + FeatureContext existingFeatureContext = existing.getRight(); + + Double existingPriority = existingFeatureContext.getPriority() == null + ? Double.POSITIVE_INFINITY + : existingFeatureContext.getPriority(); + Double featurePriority = featureContext.getPriority() == null + ? Double.POSITIVE_INFINITY + : featureContext.getPriority(); + + if (existingPriority > featurePriority) { + continue; + } + } + segmentFeatureContexts.put(featureKey, + new ImmutablePair( + segmentContext.getName(), featureContext)); + } + } + } } - return environment.getFeatureStates(); - } - - /** - * Get a specific feature state for a given feature_name in a given environment. - * - * @param environment Instance of the Environment. - * @param featureName Feature name to search for. - */ - public static FeatureStateModel getEnvironmentFeatureState(EnvironmentModel environment, - String featureName) - throws FeatureStateNotFound { - return environment.getFeatureStates() - .stream() - .filter((featureState) -> featureState - .getFeature() - .getName() - .equals(featureName)) - .findFirst().orElseThrow(() -> new FeatureStateNotFound()); - } - - /** - * Get a list of feature states for a given identity in a given environment. - * - * @param environmentModel Instance of the Environment. - * @param identityModel Instance of Identity. - */ - public static List getIdentityFeatureStates(EnvironmentModel environmentModel, - IdentityModel identityModel) { - return getIdentityFeatureStates(environmentModel, identityModel, null); - } - - /** - * Get a list of feature states for a given identity in a given environment. - * - * @param environmentModel Instance of the Environment. - * @param identityModel Instance of Identity. - */ - public static List getIdentityFeatureStates(EnvironmentModel environmentModel, - IdentityModel identityModel, - List overrideTraits) { - List featureStates = - getIdentityFeatureMap(environmentModel, identityModel, overrideTraits) - .values().stream().collect(Collectors.toList()); - if (environmentModel.getProject().getHideDisabledFlags()) { - return featureStates - .stream() - .filter((featureState) -> featureState.getEnabled()) - .collect(Collectors.toList()); + String identityKey = context.getIdentity() != null + ? context.getIdentity().getKey() + : null; + + for (FeatureContext featureContext : context.getFeatures().getAdditionalProperties().values()) { + if (segmentFeatureContexts.containsKey(featureContext.getFeatureKey())) { + ImmutablePair segmentNameWithFeatureContext = segmentFeatureContexts + .get(featureContext.getFeatureKey()); + featureContext = segmentNameWithFeatureContext.getRight(); + flags.add(new FlagResult().withEnabled(featureContext.getEnabled()) + .withFeatureKey(featureContext.getFeatureKey()) + .withName(featureContext.getName()) + .withValue(featureContext.getValue()) + .withReason("TARGETING MATCH; segment=" + segmentNameWithFeatureContext.getLeft())); + } else { + flags.add(getFlagResultFromFeatureContext(featureContext, identityKey)); + } } - return featureStates; - } - /** - * Get a specific feature state for a given identity in a given environment. - * - * @param environmentModel Instance of the Environment. - * @param identityModel Instance of identity. - * @param featureName Feature Name to search for. - * @param overrideTraits Traits to override identity's traits. - */ - public static FeatureStateModel getIdentityFeatureState(EnvironmentModel environmentModel, - IdentityModel identityModel, - String featureName, - List overrideTraits) - throws FeatureStateNotFound { - Map featureStates = - getIdentityFeatureMap(environmentModel, identityModel, overrideTraits); - - FeatureModel feature = featureStates.keySet() - .stream() - .filter((featureModel) -> featureModel.getName().equals(featureName)) - .findFirst().orElseThrow(() -> new FeatureStateNotFound()); - - return featureStates.get(feature); + return new EvaluationResult().withContext(context).withFlags(flags).withSegments(segments); } - /** - * Build a feature map with feature as key and feature state as value. - * - * @param environmentModel Instance of the Environment. - * @param identityModel Instance of identity. - * @param overrideTraits Traits to override identity's traits. - */ - private static Map getIdentityFeatureMap( - EnvironmentModel environmentModel, - IdentityModel identityModel, List overrideTraits) { - - Map featureStates = new HashMap<>(); - - if (environmentModel.getFeatureStates() != null) { - featureStates = environmentModel.getFeatureStates() - .stream() - .collect(Collectors.toMap( - FeatureStateModel::getFeature, - (featureState) -> featureState) - ); - } - - List identitySegments = - SegmentEvaluator.getIdentitySegments(environmentModel, identityModel, overrideTraits); - - for (SegmentModel segmentModel : identitySegments) { - for (FeatureStateModel featureState : segmentModel.getFeatureStates()) { - FeatureModel feature = featureState.getFeature(); - FeatureStateModel existing = featureStates.get(feature); - if (existing != null && existing.isHigherPriority(featureState)) { - continue; + private static FlagResult getFlagResultFromFeatureContext( + FeatureContext featureContext, + String identityKey) { + if (identityKey != null) { + List variants = featureContext.getVariants(); + if (variants != null) { + Float percentageValue = Hashing.getInstance() + .getHashedPercentageForObjectIds(List.of(featureContext.getKey(), identityKey)); + + Float startPercentage = 0.0f; + + for (FeatureValue variant : variants) { + 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); + } + startPercentage = limit; } - - featureStates.put(featureState.getFeature(), featureState); - } - } - - for (FeatureStateModel featureState : identityModel.getIdentityFeatures()) { - if (featureStates.containsKey(featureState.getFeature())) { - featureStates.put(featureState.getFeature(), featureState); } } - return featureStates; + return new FlagResult().withEnabled(featureContext.getEnabled()) + .withFeatureKey(featureContext.getFeatureKey()) + .withName(featureContext.getName()) + .withValue(featureContext.getValue()) + .withReason("DEFAULT"); } } \ No newline at end of file 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..73b10252 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/IdentityContext.java @@ -0,0 +1,301 @@ +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..33a58dc6 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java @@ -0,0 +1,128 @@ +package com.flagsmith.flagengine; + +import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +public class SegmentCondition { + @Getter + @Setter + private SegmentConditions operator; + @Getter + private Object value; + @Getter + @Setter + 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 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/EnvironmentModel.java b/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java deleted file mode 100644 index 58f1ac3b..00000000 --- a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.flagsmith.flagengine.environments; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.projects.ProjectModel; -import com.flagsmith.utils.models.BaseModel; -import java.util.List; -import lombok.Data; - -@Data -public class EnvironmentModel extends BaseModel { - private Integer id; - - @JsonProperty("api_key") - private String apiKey; - private ProjectModel project; - - @JsonProperty("feature_states") - private List featureStates; - - @JsonProperty("identity_overrides") - private List identityOverrides; -} 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/features/MultivariateFeatureOptionModel.java b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java deleted file mode 100644 index 6d1c6260..00000000 --- a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.flagsmith.flagengine.features; - -import com.flagsmith.utils.models.BaseModel; -import lombok.Data; - -@Data -public class MultivariateFeatureOptionModel extends BaseModel { - private String value; - private Integer id; -} diff --git a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java deleted file mode 100644 index 6e197b5a..00000000 --- a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.flagsmith.flagengine.features; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.utils.models.BaseModel; -import java.util.UUID; -import lombok.Data; - -@Data -public class MultivariateFeatureStateValueModel extends BaseModel { - @JsonProperty("multivariate_feature_option") - private MultivariateFeatureOptionModel multivariateFeatureOption; - @JsonProperty("percentage_allocation") - private Float percentageAllocation; - private Integer id; - @JsonProperty("mv_fs_value_uuid") - private String mvFsValueUuid = UUID.randomUUID().toString(); - - public Comparable getSortValue() { - return id != null ? id : mvFsValueUuid; - } -} 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 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 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/identities/traits/TraitModel.java b/src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java deleted file mode 100644 index 5864dea2..00000000 --- a/src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.flagsmith.flagengine.identities.traits; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@SuperBuilder -public class TraitModel { - @JsonProperty("trait_key") - private String traitKey; - @JsonProperty("trait_value") - private Object traitValue; -} 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/SegmentConditionModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java deleted file mode 100644 index e8956b34..00000000 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.flagsmith.flagengine.segments; - -import com.flagsmith.flagengine.segments.constants.SegmentConditions; -import lombok.Data; - -@Data -public class SegmentConditionModel { - private SegmentConditions operator; - private String value; - //CHECKSTYLE:OFF - private String property_; - //CHECKSTYLE:ON -} diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index c0b2856e..2f21c4c8 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -1,196 +1,179 @@ 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 com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Optional; 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 jacksonNodeConf = Configuration.builder() + .jsonProvider(new JacksonJsonNodeJsonProvider()) + .mappingProvider(new JacksonMappingProvider(mapper)) + .options(Option.DEFAULT_PATH_LEAF_TO_NULL) + .build(); /** - * 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.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 overrideTraits) { - List segmentRules = segment.getRules(); - List 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) { + switch (rule.getType()) { + case ALL: + return rule.getConditions().stream() + .allMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); + case ANY: + return rule.getConditions().stream() + .anyMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); + case NONE: + return rule.getConditions().stream() + .noneMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); + default: + return false; } - - return Boolean.FALSE; } - /** - * 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 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(String.class::cast) + .collect(Collectors.toList()); + } else if (conditionValue instanceof String) { + String stringConditionValue = (String) conditionValue; + try { + // Try parsing a JSON list first + conditionList = mapper.readValue( + stringConditionValue, new TypeReference>() { + }); + } 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 - )); - } + return conditionList.contains(contextValue.toString()); - return matchingCondition; - } + case PERCENTAGE_SPLIT: + String key = (contextValue != null) ? contextValue.toString() + : (context.getIdentity() != null) ? context.getIdentity().getKey() : null; - /** - * 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 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; - } - } + if (key == null) { + return false; + } - if (identityTraits != null) { - Optional matchingTrait = identityTraits - .stream() - .filter((trait) -> trait.getTraitKey().equals(condition.getProperty_())) - .findFirst(); + List objectIds = List.of(segmentKey, key); - return traitMatchesSegmentCondition(matchingTrait, condition); - } + final float floatValue; + try { + floatValue = Float.parseFloat(String.valueOf(condition.getValue())); + } catch (NumberFormatException e) { + return false; + } - return condition.getOperator().equals(SegmentConditions.IS_NOT_SET); - } + return Hashing.getInstance() + .getHashedPercentageForObjectIds(objectIds) <= floatValue; - /** - * 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 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(); - } + case IS_NOT_SET: + return contextValue == null; - return trait.isPresent() && conditionMatchesTraitValue(condition, trait.get().getTraitValue()); - } + case IS_SET: + return contextValue != null; - /** - * 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)); + return (String.valueOf(contextValue)).indexOf(conditionValue.toString()) > -1; + + case NOT_CONTAINS: + 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()); + 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) { + if (property.startsWith("$.")) { + return JsonPath.using(jacksonNodeConf).parse(mapper.valueToTree(context)).read(property); + } + if (context.getIdentity() != null) { + return context.getIdentity().getTraits().getAdditionalProperties().get(property); } + return null; } } \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java deleted file mode 100644 index dc13c2f9..00000000 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.flagsmith.flagengine.segments; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.utils.models.BaseModel; -import java.util.List; -import lombok.Data; - -@Data -public class SegmentModel extends BaseModel { - private Integer id; - private String name; - private List rules; - @JsonProperty("feature_states") - private List featureStates; - -} 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/interfaces/IOfflineHandler.java b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java index 48f6c1d9..481d6c35 100644 --- a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java +++ b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java @@ -1,7 +1,7 @@ package com.flagsmith.interfaces; -import com.flagsmith.flagengine.environments.EnvironmentModel; +import com.flagsmith.flagengine.EvaluationContext; public interface IOfflineHandler { - EnvironmentModel getEnvironment(); + EvaluationContext getEvaluationContext(); } diff --git a/src/main/resources/schema/evaluation-context.json b/src/main/resources/schema/evaluation-context.json new file mode 100644 index 00000000..673cb125 --- /dev/null +++ b/src/main/resources/schema/evaluation-context.json @@ -0,0 +1,3 @@ +{ + "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json" +} \ No newline at end of file diff --git a/src/main/resources/schema/evaluation-result.json b/src/main/resources/schema/evaluation-result.json new file mode 100644 index 00000000..03e83ff7 --- /dev/null +++ b/src/main/resources/schema/evaluation-result.json @@ -0,0 +1,3 @@ +{ + "$ref": "https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json" +} \ No newline at end of file From da5b62b374a82093c21eaf3723f5c4e6b00e86b8 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 11:32:26 +0100 Subject: [PATCH 02/62] mapper wip --- .../com/flagsmith/mappers/EngineMappers.java | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/main/java/com/flagsmith/mappers/EngineMappers.java 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..a502da35 --- /dev/null +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -0,0 +1,345 @@ +package com.flagsmith.mappers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.flagsmith.flagengine.EnvironmentContext; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.FeatureContext; +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * EngineMappers + * + *

Utility class for mapping JSON data to flag engine context objects. + */ +public class EngineMappers { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 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); + + // Map traits if provided + if (traits != null && !traits.isEmpty()) { + Traits identityTraits = new Traits(); + 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"); + } + } + identityTraits.withAdditionalProperty(entry.getKey(), traitValue); + } + identityContext.withTraits(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) { + + // Create environment context + final EnvironmentContext environmentContext = new EnvironmentContext() + .withKey(environmentDocument.get("api_key").asText()) + .withName("Test Environment"); + + // Map features + Map features = new HashMap<>(); + JsonNode featureStates = environmentDocument.get("feature_states"); + if (featureStates != null && featureStates.isArray()) { + for (JsonNode featureState : featureStates) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + features.put(featureContext.getName(), featureContext); + } + } + + // Map segments + Map segments = new HashMap<>(); + + // Map project segments + JsonNode project = environmentDocument.get("project"); + if (project != null) { + JsonNode projectSegments = project.get("segments"); + if (projectSegments != null && projectSegments.isArray()) { + for (JsonNode segment : projectSegments) { + String segmentKey = segment.get("id").asText(); + SegmentContext segmentContext = mapSegmentToSegmentContext(segment); + segments.put(segmentKey, segmentContext); + } + } + } + + // Map identity overrides + JsonNode identityOverrides = environmentDocument.get("identity_overrides"); + if (identityOverrides != null && identityOverrides.isArray()) { + Map identityOverrideSegments = + mapIdentityOverridesToSegments(identityOverrides); + segments.putAll(identityOverrideSegments); + } + + // Create evaluation context + return new EvaluationContext() + .withEnvironment(environmentContext) + .withFeatures(new com.flagsmith.flagengine.Features() + .withAdditionalProperty("features", features)) + .withSegments(new Segments() + .withAdditionalProperty("segments", segments)); + } + + /** + * Maps identity overrides to segment contexts. + * + * @param identityOverrides the identity overrides JSON array + * @return map of segment contexts + */ + private static Map mapIdentityOverridesToSegments( + JsonNode identityOverrides) { + + Map, List> featuresToIdentifiers = new HashMap<>(); + + for (JsonNode identityOverride : identityOverrides) { + JsonNode identityFeatures = identityOverride.get("identity_features"); + if (identityFeatures == null || !identityFeatures.isArray() || identityFeatures.size() == 0) { + continue; + } + + // Create overrides key + List overridesKey = new ArrayList<>(); + List sortedFeatures = new ArrayList<>(); + identityFeatures.forEach(sortedFeatures::add); + sortedFeatures.sort((a, b) -> a.get("feature").get("name").asText() + .compareTo(b.get("feature").get("name").asText())); + + for (JsonNode featureState : sortedFeatures) { + JsonNode feature = featureState.get("feature"); + overridesKey.add(feature.get("id").asText()); + overridesKey.add(feature.get("name").asText()); + overridesKey.add(featureState.get("enabled").asBoolean()); + overridesKey.add(featureState.get("feature_state_value")); + } + + String identifier = identityOverride.get("identifier").asText(); + 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(); + + // Generate unique segment key + String segmentKey = String.valueOf(overridesKey.hashCode()); + + // 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 + List overrides = new ArrayList<>(); + for (int i = 0; i < overridesKey.size(); i += 4) { + String featureKey = overridesKey.get(i).toString(); + String featureName = overridesKey.get(i + 1).toString(); + boolean featureEnabled = (Boolean) overridesKey.get(i + 2); + Object featureValue = overridesKey.get(i + 3); + + FeatureContext override = new FeatureContext() + .withKey("") + .withFeatureKey(featureKey) + .withName(featureName) + .withEnabled(featureEnabled) + .withValue(featureValue) + .withPriority(Double.NEGATIVE_INFINITY); + + overrides.add(override); + } + + SegmentContext segmentContext = new SegmentContext() + .withKey("") + .withName("identity_overrides") + .withRules(List.of(segmentRule)) + .withOverrides(overrides); + + 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 mapEnvironmentDocumentRulesToContextRules( + JsonNode rules) { + + List segmentRules = new ArrayList<>(); + + for (JsonNode rule : rules) { + // Map conditions + List conditions = new ArrayList<>(); + JsonNode ruleConditions = rule.get("conditions"); + if (ruleConditions != null && ruleConditions.isArray()) { + for (JsonNode condition : ruleConditions) { + SegmentCondition segmentCondition = new SegmentCondition() + .withProperty(condition.get("property_").asText()) + .withOperator(SegmentConditions.valueOf(condition.get("operator").asText())) + .withValue(condition.get("value")); + conditions.add(segmentCondition); + } + } + + // Map sub-rules recursively + List subRules = new ArrayList<>(); + JsonNode ruleRules = rule.get("rules"); + if (ruleRules != null && ruleRules.isArray()) { + subRules = mapEnvironmentDocumentRulesToContextRules(ruleRules); + } + + SegmentRule segmentRule = new SegmentRule() + .withType(SegmentRule.Type.valueOf(rule.get("type").asText())) + .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 mapEnvironmentDocumentFeatureStatesToFeatureContexts( + JsonNode featureStates) { + + List featureContexts = new ArrayList<>(); + + for (JsonNode featureState : featureStates) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + featureContexts.add(featureContext); + } + + return featureContexts; + } + + /** + * Maps a single feature state to feature context. + * + * @param featureState the feature state JSON + * @return the feature context + */ + private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureState) { + JsonNode feature = featureState.get("feature"); + + FeatureContext featureContext = new FeatureContext() + .withKey(featureState.get("id").asText()) + .withFeatureKey(feature.get("id").asText()) + .withName(feature.get("name").asText()) + .withEnabled(featureState.get("enabled").asBoolean()) + .withValue(featureState.get("feature_state_value")); + + // Handle multivariate feature state values + JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); + if (multivariateValues != null && multivariateValues.isArray()) { + List> variants = new ArrayList<>(); + for (JsonNode multivariateValue : multivariateValues) { + Map variant = new HashMap<>(); + variant.put("value", multivariateValue.get("multivariate_feature_option").get("value")); + variant.put("weight", multivariateValue.get("percentage_allocation").asDouble()); + variants.add(variant); + } + featureContext.withVariants(variants); + } + + // Handle priority from feature segment + JsonNode featureSegment = featureState.get("feature_segment"); + if (featureSegment != null && !featureSegment.isNull()) { + JsonNode priority = featureSegment.get("priority"); + if (priority != null && !priority.isNull()) { + featureContext.withPriority(priority.asDouble()); + } + } + + return featureContext; + } + + /** + * Maps a segment to segment context. + * + * @param segment the segment JSON + * @return the segment context + */ + private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { + String segmentKey = segment.get("id").asText(); + + // Map rules + List rules = new ArrayList<>(); + JsonNode segmentRules = segment.get("rules"); + if (segmentRules != null && segmentRules.isArray()) { + rules = mapEnvironmentDocumentRulesToContextRules(segmentRules); + } + + // Map overrides + List overrides = new ArrayList<>(); + JsonNode segmentFeatureStates = segment.get("feature_states"); + if (segmentFeatureStates != null && segmentFeatureStates.isArray()) { + overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts(segmentFeatureStates); + } + + return new SegmentContext() + .withKey(segmentKey) + .withName(segment.get("name").asText()) + .withRules(rules) + .withOverrides(overrides); + } +} \ No newline at end of file From f989a8c75c3a03dd6987cf641d92898d5f766a5c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 12:24:27 +0100 Subject: [PATCH 03/62] mapper fixes --- .../com/flagsmith/mappers/EngineMappers.java | 88 ++++++++++--------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index a502da35..a1b7a0af 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -1,10 +1,10 @@ package com.flagsmith.mappers; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.flagengine.EnvironmentContext; import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.flagengine.FeatureContext; +import com.flagsmith.flagengine.FeatureValue; import com.flagsmith.flagengine.IdentityContext; import com.flagsmith.flagengine.SegmentCondition; import com.flagsmith.flagengine.SegmentContext; @@ -16,7 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Objects; /** * EngineMappers @@ -24,9 +24,6 @@ *

Utility class for mapping JSON data to flag engine context objects. */ public class EngineMappers { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - /** * Maps context and identity data to evaluation context. * @@ -79,7 +76,7 @@ public static EvaluationContext mapEnvironmentDocumentToContext( // Create environment context final EnvironmentContext environmentContext = new EnvironmentContext() .withKey(environmentDocument.get("api_key").asText()) - .withName("Test Environment"); + .withName(environmentDocument.get("name").asText()); // Map features Map features = new HashMap<>(); @@ -116,12 +113,24 @@ public static EvaluationContext mapEnvironmentDocumentToContext( } // Create evaluation context - return new EvaluationContext() - .withEnvironment(environmentContext) - .withFeatures(new com.flagsmith.flagengine.Features() - .withAdditionalProperty("features", features)) - .withSegments(new Segments() - .withAdditionalProperty("segments", segments)); + 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; } /** @@ -133,7 +142,8 @@ public static EvaluationContext mapEnvironmentDocumentToContext( private static Map mapIdentityOverridesToSegments( JsonNode identityOverrides) { - Map, List> featuresToIdentifiers = new HashMap<>(); + // Map from sorted list of feature contexts to identifiers + Map, List> featuresToIdentifiers = new HashMap<>(); for (JsonNode identityOverride : identityOverrides) { JsonNode identityFeatures = identityOverride.get("identity_features"); @@ -141,8 +151,8 @@ private static Map mapIdentityOverridesToSegments( continue; } - // Create overrides key - List overridesKey = new ArrayList<>(); + // 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.get("feature").get("name").asText() @@ -150,10 +160,15 @@ private static Map mapIdentityOverridesToSegments( for (JsonNode featureState : sortedFeatures) { JsonNode feature = featureState.get("feature"); - overridesKey.add(feature.get("id").asText()); - overridesKey.add(feature.get("name").asText()); - overridesKey.add(featureState.get("enabled").asBoolean()); - overridesKey.add(featureState.get("feature_state_value")); + FeatureContext featureContext = new FeatureContext() + .withKey("") + .withFeatureKey(feature.get("id").asText()) + .withName(feature.get("name").asText()) + .withEnabled(featureState.get("enabled").asBoolean()) + .withValue(featureState.get("feature_state_value") != null + ? featureState.get("feature_state_value").asText() : null) + .withPriority(Double.NEGATIVE_INFINITY); // Highest possible priority + overridesKey.add(featureContext); } String identifier = identityOverride.get("identifier").asText(); @@ -161,8 +176,8 @@ private static Map mapIdentityOverridesToSegments( } Map segmentContexts = new HashMap<>(); - for (Map.Entry, List> entry : featuresToIdentifiers.entrySet()) { - List overridesKey = entry.getKey(); + for (Map.Entry, List> entry : featuresToIdentifiers.entrySet()) { + List overridesKey = entry.getKey(); List identifiers = entry.getValue(); // Generate unique segment key @@ -179,22 +194,12 @@ private static Map mapIdentityOverridesToSegments( .withType(SegmentRule.Type.ALL) .withConditions(List.of(identifierCondition)); - // Create overrides + // Create overrides from FeatureContext objects (much cleaner now!) List overrides = new ArrayList<>(); - for (int i = 0; i < overridesKey.size(); i += 4) { - String featureKey = overridesKey.get(i).toString(); - String featureName = overridesKey.get(i + 1).toString(); - boolean featureEnabled = (Boolean) overridesKey.get(i + 2); - Object featureValue = overridesKey.get(i + 3); - - FeatureContext override = new FeatureContext() - .withKey("") - .withFeatureKey(featureKey) - .withName(featureName) - .withEnabled(featureEnabled) - .withValue(featureValue) - .withPriority(Double.NEGATIVE_INFINITY); - + for (FeatureContext featureContext : overridesKey) { + // Copy the feature context for the override + FeatureContext override = new FeatureContext(featureContext) + .withKey(""); // Override the key for identity overrides overrides.add(override); } @@ -286,16 +291,17 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(featureState.get("feature_state_value")); + .withValue(featureState.get("feature_state_value") != null + ? featureState.get("feature_state_value").asText() : null); // Handle multivariate feature state values JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); if (multivariateValues != null && multivariateValues.isArray()) { - List> variants = new ArrayList<>(); + List variants = new ArrayList<>(); for (JsonNode multivariateValue : multivariateValues) { - Map variant = new HashMap<>(); - variant.put("value", multivariateValue.get("multivariate_feature_option").get("value")); - variant.put("weight", multivariateValue.get("percentage_allocation").asDouble()); + FeatureValue variant = new FeatureValue() + .withValue(multivariateValue.get("multivariate_feature_option").get("value").asText()) + .withWeight(multivariateValue.get("percentage_allocation").asDouble()); variants.add(variant); } featureContext.withVariants(variants); From 05abbe95d18413632d2f4d92a57862a0b0726e6f Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 15:05:23 +0100 Subject: [PATCH 04/62] wip + offline handlers --- .../com/flagsmith/FlagsmithApiWrapper.java | 97 +++++++------ .../java/com/flagsmith/FlagsmithClient.java | 116 ++++++---------- .../flagsmith/interfaces/FlagsmithSdk.java | 9 +- .../com/flagsmith/mappers/EngineMappers.java | 112 +++++++-------- .../flagsmith/models/FeatureStateModel.java | 131 ++++++++++++++++++ src/main/java/com/flagsmith/models/Flag.java | 4 +- src/main/java/com/flagsmith/models/Flags.java | 56 ++++++-- .../com/flagsmith/models/SdkTraitModel.java | 1 - .../java/com/flagsmith/models/TraitModel.java | 18 +++ .../flagsmith/offline/LocalFileHandler.java | 13 +- .../responses/FlagsAndTraitsResponse.java | 2 +- .../java/com/flagsmith/utils/ModelUtils.java | 29 ++-- 12 files changed, 362 insertions(+), 226 deletions(-) create mode 100644 src/main/java/com/flagsmith/models/FeatureStateModel.java create mode 100644 src/main/java/com/flagsmith/models/TraitModel.java diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index b327ad95..e605df92 100644 --- a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java +++ b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java @@ -1,15 +1,17 @@ package com.flagsmith; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.exceptions.FlagsmithRuntimeError; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; +import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.interfaces.FlagsmithSdk; +import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; +import com.flagsmith.models.TraitModel; import com.flagsmith.responses.FlagsAndTraitsResponse; import com.flagsmith.threads.AnalyticsProcessor; import com.flagsmith.threads.RequestProcessor; @@ -20,7 +22,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import lombok.Data; import lombok.Getter; import okhttp3.HttpUrl; import okhttp3.MediaType; @@ -45,19 +46,18 @@ public class FlagsmithApiWrapper implements FlagsmithSdk { /** * Instantiate with cache. * - * @param cache cache object + * @param cache cache object * @param defaultConfig config object * @param customHeaders custom headers list - * @param logger logger object - * @param apiKey api key + * @param logger logger object + * @param apiKey api key */ public FlagsmithApiWrapper( final FlagsmithCache cache, final FlagsmithConfig defaultConfig, final HashMap customHeaders, final FlagsmithLogger logger, - final String apiKey - ) { + final String apiKey) { this(defaultConfig, customHeaders, logger, apiKey); this.cache = cache; } @@ -67,15 +67,14 @@ public FlagsmithApiWrapper( * * @param defaultConfig config object * @param customHeaders custom headers list - * @param logger logger instance - * @param apiKey api key + * @param logger logger instance + * @param apiKey api key */ public FlagsmithApiWrapper( final FlagsmithConfig defaultConfig, final HashMap customHeaders, final FlagsmithLogger logger, - final String apiKey - ) { + final String apiKey) { this.defaultConfig = defaultConfig; this.customHeaders = customHeaders; this.logger = logger; @@ -83,26 +82,25 @@ public FlagsmithApiWrapper( requestor = new RequestProcessor( defaultConfig.getHttpClient(), logger, - defaultConfig.getRetries() - ); + defaultConfig.getRetries()); } /** - * Instantiate with config, custom headers, logger, apikey and request processor. + * Instantiate with config, custom headers, logger, apikey and request + * processor. * - * @param defaultConfig config object - * @param customHeaders custom headers list - * @param logger logger instance - * @param apiKey api key + * @param defaultConfig config object + * @param customHeaders custom headers list + * @param logger logger instance + * @param apiKey api key * @param requestProcessor request processor */ public FlagsmithApiWrapper( - final FlagsmithConfig defaultConfig, - final HashMap customHeaders, - final FlagsmithLogger logger, - final String apiKey, - final RequestProcessor requestProcessor - ) { + final FlagsmithConfig defaultConfig, + final HashMap customHeaders, + final FlagsmithLogger logger, + final String apiKey, + final RequestProcessor requestProcessor) { this.defaultConfig = defaultConfig; this.customHeaders = customHeaders; this.logger = logger; @@ -131,14 +129,13 @@ public Flags getFeatureFlags(boolean doThrow) { Future> featureFlagsFuture = requestor.executeAsync( request, - new TypeReference>() {}, - doThrow - ); + new TypeReference>() { + }, + doThrow); try { List featureFlagsResponse = featureFlagsFuture.get( - TIMEOUT, TimeUnit.MILLISECONDS - ); + TIMEOUT, TimeUnit.MILLISECONDS); if (featureFlagsResponse == null) { featureFlagsResponse = new ArrayList<>(); @@ -147,8 +144,7 @@ public Flags getFeatureFlags(boolean doThrow) { featureFlags = Flags.fromApiFlags( featureFlagsResponse, getConfig().getAnalyticsProcessor(), - getConfig().getFlagsmithFlagDefaults() - ); + getConfig().getFlagsmithFlagDefaults()); if (getCache() != null && getCache().getEnvFlagsCacheKey() != null) { getCache().getCache().put(getCache().getEnvFlagsCacheKey(), featureFlags); @@ -172,8 +168,7 @@ public Flags getFeatureFlags(boolean doThrow) { @Override public Flags identifyUserWithTraits( - String identifier, List traits, boolean isTransient, boolean doThrow - ) { + String identifier, List traits, boolean isTransient, boolean doThrow) { assertValidUser(identifier); Flags flags = null; String cacheKey = null; @@ -207,23 +202,22 @@ public Flags identifyUserWithTraits( Future featureFlagsFuture = requestor.executeAsync( request, - new TypeReference() {}, - doThrow - ); + new TypeReference() { + }, + doThrow); try { FlagsAndTraitsResponse flagsAndTraitsResponse = featureFlagsFuture.get( - TIMEOUT, TimeUnit.MILLISECONDS - ); + TIMEOUT, TimeUnit.MILLISECONDS); List flagsArray = flagsAndTraitsResponse != null && flagsAndTraitsResponse.getFlags() != null - ? flagsAndTraitsResponse.getFlags() : new ArrayList<>(); + ? flagsAndTraitsResponse.getFlags() + : new ArrayList<>(); flags = Flags.fromApiFlags( flagsArray, getConfig().getAnalyticsProcessor(), - getConfig().getFlagsmithFlagDefaults() - ); + getConfig().getFlagsmithFlagDefaults()); if (cacheKey != null) { getCache().getCache().put(cacheKey, flags); @@ -248,19 +242,23 @@ public Flags identifyUserWithTraits( } @Override - public EnvironmentModel getEnvironment() { + public EvaluationContext getEvaluationContext() { final Request request = newGetRequest(defaultConfig.getEnvironmentUri()); - Future environmentFuture = requestor.executeAsync(request, - new TypeReference() {}, + Future environmentFuture = requestor.executeAsync(request, + new TypeReference() { + }, Boolean.TRUE); try { - return environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + JsonNode environmentJson = environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + return EngineMappers.mapEnvironmentDocumentToContext(environmentJson); } catch (TimeoutException ie) { logger.error("Timed out on fetching Feature flags.", ie); } catch (InterruptedException ie) { logger.error("Environment loading interrupted.", ie); + } catch (IllegalArgumentException iae) { + logger.error("Environment loading failed.", iae); } catch (ExecutionException ee) { logger.error("Execution failed on Environment loading.", ee); throw new FlagsmithRuntimeError(ee); @@ -320,7 +318,7 @@ public Request newGetRequest(HttpUrl url) { /** * Returns a build request with GET. * - * @param url - URL to invoke + * @param url - URL to invoke * @param body - body to post */ @Override @@ -332,7 +330,8 @@ public Request newPostRequest(HttpUrl url, RequestBody body) { } /** - * Close the FlagsmithAPIWrapper instance, cleaning up any dependent threads or services + * Close the FlagsmithAPIWrapper instance, cleaning up any dependent threads or + * services * which need cleaning up before the instance can be fully destroyed. */ public void close() { diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 08801ab8..87f90888 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -6,18 +6,17 @@ import com.flagsmith.exceptions.FlagsmithClientError; import com.flagsmith.exceptions.FlagsmithRuntimeError; import com.flagsmith.flagengine.Engine; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import com.flagsmith.flagengine.segments.SegmentEvaluator; -import com.flagsmith.flagengine.segments.SegmentModel; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.EvaluationResult; import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.interfaces.FlagsmithSdk; +import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.BaseFlag; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.Segment; +import com.flagsmith.models.TraitModel; import com.flagsmith.threads.PollingManager; import com.flagsmith.utils.ModelUtils; import java.util.ArrayList; @@ -39,9 +38,8 @@ public class FlagsmithClient { private final FlagsmithLogger logger = new FlagsmithLogger(); private FlagsmithSdk flagsmithSdk; - private EnvironmentModel environment; + private EvaluationContext evaluationContext; private PollingManager pollingManager; - private Map identitiesWithOverridesByIdentifier; private FlagsmithClient() { } @@ -55,22 +53,12 @@ public static FlagsmithClient.Builder newBuilder() { */ public void updateEnvironment() { try { - EnvironmentModel updatedEnvironment = flagsmithSdk.getEnvironment(); + EvaluationContext updatedEvaluationContext = flagsmithSdk.getEvaluationContext(); // if we didn't get an environment from the API, // then don't overwrite the copy we already have. - if (updatedEnvironment != null) { - List identityOverrides = updatedEnvironment.getIdentityOverrides(); - - if (identityOverrides != null) { - Map identitiesWithOverridesByIdentifier = new HashMap<>(); - for (IdentityModel identity : identityOverrides) { - identitiesWithOverridesByIdentifier.put(identity.getIdentifier(), identity); - } - this.identitiesWithOverridesByIdentifier = identitiesWithOverridesByIdentifier; - } - - this.environment = updatedEnvironment; + if (updatedEvaluationContext != null) { + this.evaluationContext = updatedEvaluationContext; } else { logger.error(getEnvironmentUpdateErrorMessage()); } @@ -150,13 +138,10 @@ public Flags getIdentityFlags(String identifier, Map traits) public Flags getIdentityFlags(String identifier, Map traits, boolean isTransient) throws FlagsmithClientError { if (getShouldUseEnvironmentDocument()) { - return getIdentityFlagsFromDocument( - identifier, - ModelUtils.getTraitModelsFromTraitMap(traits)); + return getIdentityFlagsFromDocument(identifier, traits); } - return getIdentityFlagsFromApi( - identifier, ModelUtils.getSdkTraitModelsFromTraitMap(traits), isTransient); + return getIdentityFlagsFromApi(identifier, traits, isTransient); } /** @@ -180,20 +165,18 @@ public List getIdentitySegments(String identifier) */ public List getIdentitySegments(String identifier, Map traits) throws FlagsmithClientError { - if (environment == null) { + if (evaluationContext == null) { throw new FlagsmithClientError("Local evaluation required to obtain identity segments."); } - IdentityModel identityModel = getIdentityModel( - identifier, - (traits != null - ? ModelUtils.getTraitModelsFromTraitMap(traits) - : new ArrayList())); - List segmentModels = SegmentEvaluator.getIdentitySegments( - environment, identityModel); - - return segmentModels.stream().map((segmentModel) -> { + + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + evaluationContext, identifier, traits); + + final EvaluationResult result = Engine.getEvaluationResult(context); + + return result.getSegments().stream().map((segmentModel) -> { Segment segment = new Segment(); - segment.setId(segmentModel.getId()); + segment.setId(Integer.valueOf(segmentModel.getKey())); segment.setName(segmentModel.getName()); return segment; @@ -212,37 +195,39 @@ public void close() { } private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError { - if (environment == null) { + if (evaluationContext == null) { if (getConfig().getFlagsmithFlagDefaults() == null) { throw new FlagsmithClientError("Unable to get flags. No environment present."); } return getDefaultFlags(); } - return Flags.fromFeatureStateModels( - Engine.getEnvironmentFeatureStates(environment), + final EvaluationResult result = Engine.getEvaluationResult(evaluationContext); + + return Flags.fromEvaluationResult( + result, getConfig().getAnalyticsProcessor(), - null, getConfig().getFlagsmithFlagDefaults()); } private Flags getIdentityFlagsFromDocument( - String identifier, List traitModels) + String identifier, Map traits) throws FlagsmithClientError { - if (environment == null) { + if (evaluationContext == null) { if (getConfig().getFlagsmithFlagDefaults() == null) { throw new FlagsmithClientError("Unable to get flags. No environment present."); } return getDefaultFlags(); } - IdentityModel identity = getIdentityModel(identifier, traitModels); - List featureStates = Engine.getIdentityFeatureStates(environment, identity); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + evaluationContext, identifier, traits); - return Flags.fromFeatureStateModels( - featureStates, + final EvaluationResult result = Engine.getEvaluationResult(context); + + return Flags.fromEvaluationResult( + result, getConfig().getAnalyticsProcessor(), - identity.getCompositeKey(), getConfig().getFlagsmithFlagDefaults()); } @@ -252,7 +237,7 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError { } catch (Exception e) { if (getConfig().getFlagsmithFlagDefaults() != null) { return getDefaultFlags(); - } else if (environment != null) { + } else if (evaluationContext != null) { try { return getEnvironmentFlagsFromDocument(); } catch (FlagsmithClientError ce) { @@ -265,20 +250,20 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError { } private Flags getIdentityFlagsFromApi( - String identifier, List traitModels, boolean isTransient) + String identifier, Map traits, boolean isTransient) throws FlagsmithApiError { try { return flagsmithSdk.identifyUserWithTraits( identifier, - traitModels, + ModelUtils.getSdkTraitModelsFromTraitMap(traits), isTransient, Boolean.TRUE); } catch (Exception e) { if (getConfig().getFlagsmithFlagDefaults() != null) { return getDefaultFlags(); - } else if (environment != null) { + } else if (evaluationContext != null) { try { - return getIdentityFlagsFromDocument(identifier, traitModels); + return getIdentityFlagsFromDocument(identifier, traits); } catch (FlagsmithClientError ce) { // Do nothing and fall through to FlagsmithApiError } @@ -288,29 +273,6 @@ private Flags getIdentityFlagsFromApi( } } - private IdentityModel getIdentityModel(String identifier, List traitModels) - throws FlagsmithClientError { - if (environment == null) { - throw new FlagsmithClientError( - "Unable to build identity model when no local environment present."); - } - - if (identitiesWithOverridesByIdentifier != null) { - IdentityModel identityOverride = identitiesWithOverridesByIdentifier.get(identifier); - if (identityOverride != null) { - identityOverride.updateTraits(traitModels); - return identityOverride; - } - } - - IdentityModel identity = new IdentityModel(); - identity.setIdentityTraits(traitModels); - identity.setEnvironmentApiKey(environment.getApiKey()); - identity.setIdentifier(identifier); - - return identity; - } - private Flags getDefaultFlags() { Flags flags = new Flags(); flags.setDefaultFlagHandler(getConfig().getFlagsmithFlagDefaults()); @@ -318,7 +280,7 @@ private Flags getDefaultFlags() { } private String getEnvironmentUpdateErrorMessage() { - if (this.environment == null) { + if (this.evaluationContext == null) { return "Unable to update environment from API. " + "No environment configured - using defaultHandler if configured."; } else { diff --git a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java index c3283d14..7d6b4619 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; @@ -16,12 +16,11 @@ public interface FlagsmithSdk { Flags getFeatureFlags(boolean doThrow); Flags identifyUserWithTraits( - String identifier, List traits, boolean isTransient, boolean doThrow - ); + String identifier, List traits, boolean isTransient, boolean doThrow); FlagsmithConfig getConfig(); - EnvironmentModel getEnvironment(); + EvaluationContext getEvaluationContext(); RequestProcessor getRequestor(); diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index a1b7a0af..ce542144 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -16,7 +16,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; /** * EngineMappers @@ -27,21 +26,21 @@ public class EngineMappers { /** * Maps context and identity data to evaluation context. * - * @param context the base evaluation context - * @param identifier the identity identifier - * @param traits optional traits mapping + * @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); - + // Map traits if provided if (traits != null && !traits.isEmpty()) { Traits identityTraits = new Traits(); @@ -58,7 +57,7 @@ public static EvaluationContext mapContextAndIdentityDataToContext( } identityContext.withTraits(identityTraits); } - + // Create new evaluation context with identity return new EvaluationContext(context) .withIdentity(identityContext); @@ -72,12 +71,12 @@ public static EvaluationContext mapContextAndIdentityDataToContext( */ public static EvaluationContext mapEnvironmentDocumentToContext( JsonNode environmentDocument) { - + // Create environment context final EnvironmentContext environmentContext = new EnvironmentContext() - .withKey(environmentDocument.get("api_key").asText()) - .withName(environmentDocument.get("name").asText()); - + .withKey(environmentDocument.get("api_key").require().asText()) + .withName(environmentDocument.get("name").require().asText()); + // Map features Map features = new HashMap<>(); JsonNode featureStates = environmentDocument.get("feature_states"); @@ -87,10 +86,10 @@ public static EvaluationContext mapEnvironmentDocumentToContext( features.put(featureContext.getName(), featureContext); } } - + // Map segments Map segments = new HashMap<>(); - + // Map project segments JsonNode project = environmentDocument.get("project"); if (project != null) { @@ -103,33 +102,33 @@ public static EvaluationContext mapEnvironmentDocumentToContext( } } } - + // Map identity overrides JsonNode identityOverrides = environmentDocument.get("identity_overrides"); if (identityOverrides != null && identityOverrides.isArray()) { - Map identityOverrideSegments = + Map identityOverrideSegments = mapIdentityOverridesToSegments(identityOverrides); 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; } @@ -141,23 +140,23 @@ public static EvaluationContext mapEnvironmentDocumentToContext( */ private static Map mapIdentityOverridesToSegments( JsonNode identityOverrides) { - + // Map from sorted list of feature contexts to identifiers Map, List> featuresToIdentifiers = new HashMap<>(); - + for (JsonNode identityOverride : identityOverrides) { JsonNode identityFeatures = identityOverride.get("identity_features"); if (identityFeatures == null || !identityFeatures.isArray() || identityFeatures.size() == 0) { 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.get("feature").get("name").asText() .compareTo(b.get("feature").get("name").asText())); - + for (JsonNode featureState : sortedFeatures) { JsonNode feature = featureState.get("feature"); FeatureContext featureContext = new FeatureContext() @@ -165,35 +164,36 @@ private static Map mapIdentityOverridesToSegments( .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(featureState.get("feature_state_value") != null - ? featureState.get("feature_state_value").asText() : null) - .withPriority(Double.NEGATIVE_INFINITY); // Highest possible priority + .withValue(featureState.get("feature_state_value") != null + ? featureState.get("feature_state_value").asText() + : null) + .withPriority(Double.NEGATIVE_INFINITY); // Highest possible priority overridesKey.add(featureContext); } - + String identifier = identityOverride.get("identifier").asText(); 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(); - + // Generate unique segment key String segmentKey = String.valueOf(overridesKey.hashCode()); - + // 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 (much cleaner now!) List overrides = new ArrayList<>(); for (FeatureContext featureContext : overridesKey) { @@ -202,16 +202,16 @@ private static Map mapIdentityOverridesToSegments( .withKey(""); // Override the key for identity overrides overrides.add(override); } - + SegmentContext segmentContext = new SegmentContext() .withKey("") .withName("identity_overrides") .withRules(List.of(segmentRule)) .withOverrides(overrides); - + segmentContexts.put(segmentKey, segmentContext); } - + return segmentContexts; } @@ -223,9 +223,9 @@ private static Map mapIdentityOverridesToSegments( */ private static List mapEnvironmentDocumentRulesToContextRules( JsonNode rules) { - + List segmentRules = new ArrayList<>(); - + for (JsonNode rule : rules) { // Map conditions List conditions = new ArrayList<>(); @@ -239,22 +239,22 @@ private static List mapEnvironmentDocumentRulesToContextRules( conditions.add(segmentCondition); } } - + // Map sub-rules recursively List subRules = new ArrayList<>(); JsonNode ruleRules = rule.get("rules"); if (ruleRules != null && ruleRules.isArray()) { subRules = mapEnvironmentDocumentRulesToContextRules(ruleRules); } - + SegmentRule segmentRule = new SegmentRule() .withType(SegmentRule.Type.valueOf(rule.get("type").asText())) .withConditions(conditions) .withRules(subRules); - + segmentRules.add(segmentRule); } - + return segmentRules; } @@ -266,14 +266,14 @@ private static List mapEnvironmentDocumentRulesToContextRules( */ private static List mapEnvironmentDocumentFeatureStatesToFeatureContexts( JsonNode featureStates) { - + List featureContexts = new ArrayList<>(); - + for (JsonNode featureState : featureStates) { FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); featureContexts.add(featureContext); } - + return featureContexts; } @@ -285,20 +285,24 @@ private static List mapEnvironmentDocumentFeatureStatesToFeature */ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureState) { JsonNode feature = featureState.get("feature"); - + FeatureContext featureContext = new FeatureContext() .withKey(featureState.get("id").asText()) .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(featureState.get("feature_state_value") != null - ? featureState.get("feature_state_value").asText() : null); - + .withValue(featureState.get("feature_state_value") != null + ? featureState.get("feature_state_value").asText() + : null); + // Handle multivariate feature state values JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); if (multivariateValues != null && multivariateValues.isArray()) { List variants = new ArrayList<>(); - for (JsonNode multivariateValue : multivariateValues) { + List sortedMultivariate = new ArrayList<>(); + multivariateValues.forEach(sortedMultivariate::add); + sortedMultivariate.sort((a, b) -> a.get("id").asText().compareTo(b.get("id").asText())); + for (JsonNode multivariateValue : sortedMultivariate) { FeatureValue variant = new FeatureValue() .withValue(multivariateValue.get("multivariate_feature_option").get("value").asText()) .withWeight(multivariateValue.get("percentage_allocation").asDouble()); @@ -306,7 +310,7 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt } featureContext.withVariants(variants); } - + // Handle priority from feature segment JsonNode featureSegment = featureState.get("feature_segment"); if (featureSegment != null && !featureSegment.isNull()) { @@ -315,7 +319,7 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt featureContext.withPriority(priority.asDouble()); } } - + return featureContext; } @@ -327,21 +331,21 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt */ private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { String segmentKey = segment.get("id").asText(); - + // Map rules List rules = new ArrayList<>(); JsonNode segmentRules = segment.get("rules"); if (segmentRules != null && segmentRules.isArray()) { rules = mapEnvironmentDocumentRulesToContextRules(segmentRules); } - + // Map overrides List overrides = new ArrayList<>(); JsonNode segmentFeatureStates = segment.get("feature_states"); if (segmentFeatureStates != null && segmentFeatureStates.isArray()) { overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts(segmentFeatureStates); } - + return new SegmentContext() .withKey(segmentKey) .withName(segment.get("name").asText()) diff --git a/src/main/java/com/flagsmith/models/FeatureStateModel.java b/src/main/java/com/flagsmith/models/FeatureStateModel.java new file mode 100644 index 00000000..54c42e7e --- /dev/null +++ b/src/main/java/com/flagsmith/models/FeatureStateModel.java @@ -0,0 +1,131 @@ +package com.flagsmith.models; + +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 { + @Data + public class FeatureModel { + @JsonProperty("id") + private Integer id; + @JsonProperty("name") + private String name; + @JsonProperty("type") + private String type; + } + + @Data + public class MultivariateFeatureOptionModel { + @JsonProperty("id") + private Integer id; + @JsonProperty("value") + private String value; + } + + @Data + public class FeatureSegmentModel { + @JsonProperty("id") + private Integer id; + @JsonProperty("priority") + private Integer priority; + } + + @Data + public class MultivariateFeatureStateValueModel { + @JsonProperty("multivariate_feature_option") + private MultivariateFeatureOptionModel multivariateFeatureOption; + @JsonProperty("percentage_allocation") + private Float percentageAllocation; + @JsonProperty("id") + private Integer id; + + public Float getSortValue() { + return percentageAllocation != null ? percentageAllocation : 0f; + } + } + + 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; + } + + /** + * 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/models/Flag.java b/src/main/java/com/flagsmith/models/Flag.java index 1a24e772..1c9e3a45 100644 --- a/src/main/java/com/flagsmith/models/Flag.java +++ b/src/main/java/com/flagsmith/models/Flag.java @@ -1,9 +1,7 @@ package com.flagsmith.models; import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.flagengine.features.FeatureStateModel; import lombok.Data; -import lombok.ToString; @Data public class Flag extends BaseFlag { @@ -14,7 +12,7 @@ public class Flag extends BaseFlag { * return flag from feature state model and identity id. * * @param featureState feature state model - * @param identityId identity id + * @param identityId identity id */ public static Flag fromFeatureStateModel(FeatureStateModel featureState, Object identityId) { Flag flag = new Flag(); diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index 4961d22e..d6957f47 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -4,7 +4,7 @@ import com.flagsmith.FlagsmithFlagDefaults; import com.flagsmith.exceptions.FeatureNotFoundError; import com.flagsmith.exceptions.FlagsmithClientError; -import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.flagengine.EvaluationResult; import com.flagsmith.interfaces.DefaultFlagHandler; import com.flagsmith.threads.AnalyticsProcessor; import java.util.HashMap; @@ -22,7 +22,7 @@ public class Flags { /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor */ public static Flags fromFeatureStateModels( @@ -34,9 +34,9 @@ public static Flags fromFeatureStateModels( /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor - * @param identityId identity ID (optional) + * @param identityId identity ID (optional) */ public static Flags fromFeatureStateModels( List featureStates, @@ -48,9 +48,9 @@ public static Flags fromFeatureStateModels( /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor - * @param identityId identity ID (optional) + * @param identityId identity ID (optional) * @param defaultFlagHandler default flags (optional) */ public static Flags fromFeatureStateModels( @@ -62,8 +62,7 @@ public static Flags fromFeatureStateModels( .collect( Collectors.toMap( (fs) -> fs.getFeature().getName(), - (fs) -> Flag.fromFeatureStateModel(fs, identityId) - )); + (fs) -> Flag.fromFeatureStateModel(fs, identityId))); Flags flags = new Flags(); flags.setFlags(flagMap); @@ -76,7 +75,7 @@ public static Flags fromFeatureStateModels( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -90,8 +89,7 @@ public static Flags fromApiFlags( for (JsonNode node : apiFlags) { flagMap.put( node.get("feature").get("name").asText(), - Flag.fromApiFlag(node) - ); + Flag.fromApiFlag(node)); } Flags flags = new Flags(); @@ -105,7 +103,7 @@ public static Flags fromApiFlags( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -119,8 +117,7 @@ public static Flags fromApiFlags( for (FeatureStateModel flag : apiFlags) { flagMap.put( flag.getFeature().getName(), - Flag.fromFeatureStateModel(flag, null) - ); + Flag.fromFeatureStateModel(flag, null)); } Flags flags = new Flags(); @@ -131,6 +128,37 @@ public static Flags fromApiFlags( return flags; } + /** + * Build flags object from evaluation result. + * + * @param evaluationResult evaluation result + * @param analyticsProcessor instance of analytics processor + * @param defaultFlagHandler handler for default flags if present + */ + public static Flags fromEvaluationResult( + EvaluationResult evaluationResult, + AnalyticsProcessor analyticsProcessor, + DefaultFlagHandler defaultFlagHandler) { + Map flagMap = evaluationResult.getFlags().stream() + .collect( + Collectors.toMap( + (fs) -> fs.getFeatureKey(), + (fs) -> { + Flag flag = new Flag(); + flag.setFeatureName(fs.getName()); + flag.setValue(fs.getValue()); + flag.setEnabled(fs.getEnabled()); + return flag; + })); + + Flags flags = new Flags(); + flags.setFlags(flagMap); + flags.setAnalyticsProcessor(analyticsProcessor); + flags.setDefaultFlagHandler(defaultFlagHandler); + + return flags; + } + /** * returns the list of all flags. */ diff --git a/src/main/java/com/flagsmith/models/SdkTraitModel.java b/src/main/java/com/flagsmith/models/SdkTraitModel.java index fce8f061..fbdcb246 100644 --- a/src/main/java/com/flagsmith/models/SdkTraitModel.java +++ b/src/main/java/com/flagsmith/models/SdkTraitModel.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.identities.traits.TraitModel; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/com/flagsmith/models/TraitModel.java b/src/main/java/com/flagsmith/models/TraitModel.java new file mode 100644 index 00000000..9d287710 --- /dev/null +++ b/src/main/java/com/flagsmith/models/TraitModel.java @@ -0,0 +1,18 @@ +package com.flagsmith.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +public class TraitModel { + @JsonProperty("trait_key") + private String traitKey; + @JsonProperty("trait_value") + private Object traitValue; +} diff --git a/src/main/java/com/flagsmith/offline/LocalFileHandler.java b/src/main/java/com/flagsmith/offline/LocalFileHandler.java index 562f1b0b..2cd91fdd 100644 --- a/src/main/java/com/flagsmith/offline/LocalFileHandler.java +++ b/src/main/java/com/flagsmith/offline/LocalFileHandler.java @@ -1,15 +1,17 @@ package com.flagsmith.offline; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.MapperFactory; import com.flagsmith.exceptions.FlagsmithClientError; -import com.flagsmith.flagengine.environments.EnvironmentModel; +import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.interfaces.IOfflineHandler; +import com.flagsmith.mappers.EngineMappers; import java.io.File; import java.io.IOException; public class LocalFileHandler implements IOfflineHandler { - private EnvironmentModel environmentModel; + private EvaluationContext evaluationContext; private ObjectMapper objectMapper = MapperFactory.getMapper(); /** @@ -20,13 +22,14 @@ public class LocalFileHandler implements IOfflineHandler { public LocalFileHandler(String filePath) throws FlagsmithClientError { File file = new File(filePath); try { - environmentModel = objectMapper.readValue(file, EnvironmentModel.class); + JsonNode environmentDocument = objectMapper.readValue(file, JsonNode.class); + this.evaluationContext = EngineMappers.mapEnvironmentDocumentToContext(environmentDocument); } catch (IOException e) { throw new FlagsmithClientError("Unable to read environment from file " + filePath); } } - public EnvironmentModel getEnvironment() { - return environmentModel; + public EvaluationContext getEvaluationContext() { + return evaluationContext; } } diff --git a/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java b/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java index 12cffca5..4d3fcbe5 100644 --- a/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java +++ b/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java @@ -1,7 +1,7 @@ package com.flagsmith.responses; import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.models.FeatureStateModel; import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/flagsmith/utils/ModelUtils.java b/src/main/java/com/flagsmith/utils/ModelUtils.java index 7128d383..d5faa697 100644 --- a/src/main/java/com/flagsmith/utils/ModelUtils.java +++ b/src/main/java/com/flagsmith/utils/ModelUtils.java @@ -1,8 +1,8 @@ package com.flagsmith.utils; -import com.flagsmith.flagengine.identities.traits.TraitModel; import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.TraitConfig; +import com.flagsmith.models.TraitModel; import java.util.AbstractMap; import java.util.List; import java.util.Map; @@ -22,7 +22,7 @@ public class ModelUtils { */ public static List getTraitModelsFromTraitMap(Map traits) { return ModelUtils.getTraitModelStreamFromTraitMap( - traits, () -> new TraitModel()).map(Pair::getLeft).collect(Collectors.toList()); + traits, () -> new TraitModel()).map(Pair::getLeft).collect(Collectors.toList()); } /** @@ -34,28 +34,23 @@ public static List getTraitModelsFromTraitMap(Map tr */ public static List getSdkTraitModelsFromTraitMap(Map traits) { return ModelUtils.getTraitModelStreamFromTraitMap(traits, () -> new SdkTraitModel()).map( - (row) -> { - SdkTraitModel sdkTraitModel = row.getLeft(); - TraitConfig traitConfig = row.getRight(); - sdkTraitModel.setIsTransient(traitConfig.getIsTransient()); - return sdkTraitModel; - } - ).collect(Collectors.toList()); + (row) -> { + SdkTraitModel sdkTraitModel = row.getLeft(); + TraitConfig traitConfig = row.getRight(); + sdkTraitModel.setIsTransient(traitConfig.getIsTransient()); + return sdkTraitModel; + }).collect(Collectors.toList()); } private static Stream> getTraitConfigStreamFromTraitMap( - Map traits - ) { + Map traits) { return traits.entrySet().stream().map( row -> new AbstractMap.SimpleEntry<>( - row.getKey(), TraitConfig.fromObject(row.getValue())) - ); + row.getKey(), TraitConfig.fromObject(row.getValue()))); } - private static Stream> - getTraitModelStreamFromTraitMap( - Map traits, Supplier traitSupplier - ) { + private static Stream> + getTraitModelStreamFromTraitMap(Map traits, Supplier traitSupplier) { return ModelUtils.getTraitConfigStreamFromTraitMap(traits).map( (row) -> { T trait = traitSupplier.get(); From 9f25722ea992b6071bd83ba81b4c6c7914f1f08c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 15:19:53 +0100 Subject: [PATCH 05/62] wip + working build --- pom.xml | 7 +++++++ src/main/java/com/flagsmith/FlagsmithClient.java | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 1b4cadc2..ff9ecfde 100644 --- a/pom.xml +++ b/pom.xml @@ -224,6 +224,13 @@ 1.8 1.8 + + + org.projectlombok + lombok + ${lombok.version} + + diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 87f90888..c35d4dfc 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -345,7 +345,8 @@ public Builder setApiKey(String apiKey) { * return null by * default. * - *

If you would like to override this default behaviour, you can use this + *

+ * If you would like to override this default behaviour, you can use this * method. By default * it will return null for any flags that it does not recognise. * @@ -442,7 +443,8 @@ public Builder withCustomHttpHeaders(HashMap customHeaders) { /** * Enable in-memory caching for the Flagsmith API. * - *

If no other cache configuration is set, the Caffeine defaults will be used, + *

+ * If no other cache configuration is set, the Caffeine defaults will be used, * i.e. no limit * * @param cacheConfig an FlagsmithCacheConfig. @@ -536,7 +538,7 @@ public FlagsmithClient build() { throw new FlagsmithRuntimeError( "Cannot use both default flag handler and offline handler."); } - client.environment = configuration.getOfflineHandler().getEnvironment(); + client.evaluationContext = configuration.getOfflineHandler().getEvaluationContext(); } return this.client; From 8be5116e4ac5c957139f0c862006286dff58968b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 17:48:41 +0100 Subject: [PATCH 06/62] wip + fix build errors for tests --- .../java/com/flagsmith/FlagsmithClient.java | 6 +- .../com/flagsmith/DummyOfflineHandler.java | 8 +- .../FlagsmithApiWrapperCachingTest.java | 4 +- .../flagsmith/FlagsmithApiWrapperTest.java | 28 +- .../com/flagsmith/FlagsmithClientTest.java | 1708 ++++++++--------- .../com/flagsmith/FlagsmithTestHelper.java | 243 +-- .../com/flagsmith/flagengine/EngineTest.java | 98 +- .../fixtures/FlagEngineFixtures.java | 178 -- .../helpers/FeatureStateHelper.java | 25 - .../flagengine/models/ResponseJSON.java | 7 +- .../unit/Identities/IdentitiesModelTest.java | 97 - .../unit/Identities/IdentitiesTest.java | 99 - .../unit/environments/EnvironmentTest.java | 137 -- .../unit/feature/FeatureModelTest.java | 123 -- .../unit/feature/FeatureStateModelTest.java | 53 - .../unit/organizations/OrganizationsTest.java | 27 - .../segments/IdentitySegmentFixtures.java | 231 +-- .../unit/segments/SegmentEvaluatorTest.java | 118 +- .../unit/segments/SegmentModelTest.java | 52 +- .../offline/LocalFileHandlerTest.java | 28 +- 20 files changed, 1210 insertions(+), 2060 deletions(-) delete mode 100644 src/test/java/com/flagsmith/flagengine/fixtures/FlagEngineFixtures.java delete mode 100644 src/test/java/com/flagsmith/flagengine/helpers/FeatureStateHelper.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesModelTest.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesTest.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/feature/FeatureModelTest.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/feature/FeatureStateModelTest.java delete mode 100644 src/test/java/com/flagsmith/flagengine/unit/organizations/OrganizationsTest.java diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index c35d4dfc..2dd85f22 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -345,8 +345,7 @@ public Builder setApiKey(String apiKey) { * return null by * default. * - *

- * If you would like to override this default behaviour, you can use this + *

If you would like to override this default behaviour, you can use this * method. By default * it will return null for any flags that it does not recognise. * @@ -443,8 +442,7 @@ public Builder withCustomHttpHeaders(HashMap customHeaders) { /** * Enable in-memory caching for the Flagsmith API. * - *

- * If no other cache configuration is set, the Caffeine defaults will be used, + *

If no other cache configuration is set, the Caffeine defaults will be used, * i.e. no limit * * @param cacheConfig an FlagsmithCacheConfig. diff --git a/src/test/java/com/flagsmith/DummyOfflineHandler.java b/src/test/java/com/flagsmith/DummyOfflineHandler.java index f3fdbaeb..454256a7 100644 --- a/src/test/java/com/flagsmith/DummyOfflineHandler.java +++ b/src/test/java/com/flagsmith/DummyOfflineHandler.java @@ -1,10 +1,10 @@ package com.flagsmith; -import com.flagsmith.flagengine.environments.EnvironmentModel; +import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.interfaces.IOfflineHandler; public class DummyOfflineHandler implements IOfflineHandler { - public EnvironmentModel getEnvironment() { - return FlagsmithTestHelper.environmentModel(); - } + public EvaluationContext getEvaluationContext() { + return FlagsmithTestHelper.evaluationContext(); + } } diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java index c99fd1bb..cba467c1 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java @@ -14,10 +14,10 @@ import com.flagsmith.config.FlagsmithCacheConfig; import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.config.Retry; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; import com.flagsmith.interfaces.FlagsmithCache; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; +import com.flagsmith.models.TraitModel; import com.flagsmith.responses.FlagsAndTraitsResponse; import com.flagsmith.threads.RequestProcessor; import com.github.benmanes.caffeine.cache.Cache; diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java index 102804f6..2d2c27d2 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java @@ -18,12 +18,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.config.Retry; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; import com.flagsmith.models.BaseFlag; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flag; import com.flagsmith.models.Flags; +import com.flagsmith.models.TraitModel; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -40,7 +40,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - public class FlagsmithApiWrapperTest { private final String API_KEY = "OUR_API_KEY"; @@ -81,7 +80,8 @@ void getFeatureFlags_retries() { } // Assert - // Since the Retry object is local to the call, the only external behaviour we can watch + // Since the Retry object is local to the call, the only external behaviour we + // can watch // is the logger verify(flagsmithLogger, times(2)).httpError(any(), any(Response.class), anyBoolean()); } @@ -124,22 +124,21 @@ public void getFeatureFlags_noUser_fail() { public void identifyUserWithTraits_success() throws JsonProcessingException { // Arrange final List traits = new ArrayList(Arrays.asList(new TraitModel())); - String responseBody = mapper.writeValueAsString(getFlagsAndTraitsResponse(Arrays.asList(getNewFlag()), Arrays.asList(new TraitModel()))); + String responseBody = mapper + .writeValueAsString(getFlagsAndTraitsResponse(Arrays.asList(getNewFlag()), Arrays.asList(new TraitModel()))); interceptor.addRule() .post(BASE_URL + "/identities/") .respond(responseBody, MEDIATYPE_JSON); // Act final Flags actualFeatureFlags = sut.identifyUserWithTraits( - "user-w-traits", traits, false, true - ); + "user-w-traits", traits, false, true); // Assert Map flag1 = newFlagsList(Arrays.asList(getNewFlag())).getFlags(); Map flag2 = actualFeatureFlags.getFlags(); assertEquals( - flag1, flag2 - ); + flag1, flag2); verify(flagsmithLogger, times(1)).info(anyString(), any(), any()); verify(flagsmithLogger, times(0)).httpError(any(), any(Response.class), anyBoolean()); verify(flagsmithLogger, times(0)).httpError(any(), any(IOException.class), anyBoolean()); @@ -168,7 +167,7 @@ public void testClose_ClosesRequestProcessor() { // Given RequestProcessor mockedRequestProcessor = mock(RequestProcessor.class); FlagsmithApiWrapper apiWrapper = new FlagsmithApiWrapper( - defaultConfig, null, flagsmithLogger, API_KEY, mockedRequestProcessor); + defaultConfig, null, flagsmithLogger, API_KEY, mockedRequestProcessor); // When apiWrapper.close(); @@ -193,10 +192,10 @@ public void testClose_ClosesAnalyticsProcessor() { } private FeatureStateModel getNewFlag() { - final FeatureModel feature = new FeatureModel(); + final FeatureStateModel flag = new FeatureStateModel(); + final FeatureStateModel.FeatureModel feature = flag.new FeatureModel(); feature.setName("my-test-flag"); feature.setId(123); - final FeatureStateModel flag = new FeatureStateModel(); flag.setFeature(feature); return flag; @@ -204,8 +203,7 @@ private FeatureStateModel getNewFlag() { private Flags newFlagsList(List flags) { return Flags.fromApiFlags( - flags, null, defaultConfig.getFlagsmithFlagDefaults() - ); + flags, null, defaultConfig.getFlagsmithFlagDefaults()); } private JsonNode getFlagsAndTraitsResponse(List flags, List traits) { diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 98940c74..4b68e1e7 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -11,7 +11,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -20,17 +19,17 @@ import com.flagsmith.exceptions.FlagsmithApiError; import com.flagsmith.exceptions.FlagsmithClientError; import com.flagsmith.exceptions.FlagsmithRuntimeError; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.SegmentContext; import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.models.BaseFlag; import com.flagsmith.models.DefaultFlag; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.Segment; import com.flagsmith.models.TraitConfig; +import com.flagsmith.models.TraitModel; import com.flagsmith.responses.FlagsAndTraitsResponse; import com.flagsmith.threads.PollingManager; import com.flagsmith.threads.RequestProcessor; @@ -63,866 +62,867 @@ */ public class FlagsmithClientTest { - private static String DEFAULT_FLAG_VALUE = "foobar"; - private static boolean DEFAULT_FLAG_STATE = true; - - private static BaseFlag defaultHandler(String featureName) { - DefaultFlag defaultFlag = new DefaultFlag(); - defaultFlag.setEnabled(DEFAULT_FLAG_STATE); - defaultFlag.setValue(DEFAULT_FLAG_VALUE); - defaultFlag.setFeatureName(featureName); - return defaultFlag; - } - - @Test - public void testClient_When_Cache_Disabled_Return_Null() { - FlagsmithClient client = FlagsmithClient.newBuilder() - .setApiKey("api-key") - .build(); - - FlagsmithCache cache = client.getCache(); - - assertNull(cache); - } - - @Test - public void testClient_validateObjectCreation() throws InterruptedException { - PollingManager manager = mock(PollingManager.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withPollingManager(manager) - .withConfiguration( - FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) - .setApiKey("ser.abcdefg") - .build(); - - Thread.sleep(10); - verify(manager, times(1)).startPolling(); - } - - @Test - public void testLocalEvaluationRequiresServerKey() throws InterruptedException { - assertThrows(RuntimeException.class, () -> FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) - .setApiKey("not-a-server-key") - .build()); - } - - @Test - public void testClient_errorEnvironmentApi() { - Logger logger = mock(Logger.class); - - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .enableLogging(logger) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - client.updateEnvironment(); - - // Verify that an error was written to the log by mocking the logger and checking that a call was made - // with the expected log message. Note that the logger will also have other invocations so we need to - // iterate over them to check that the one we expect has been made. - boolean found = false; - String expectedMsg = "Unable to update environment from API. No environment configured - using defaultHandler if configured."; - for (Invocation invocation : Mockito.mockingDetails(logger).getInvocations().stream().collect(Collectors.toList())) { - if (invocation.getArgument(0).toString().contains(expectedMsg)) { - found = true; - } - } - assertTrue(found); - } - - @Test - public void testClient_validateEnvironment() - throws JsonProcessingException { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - MapperFactory.getMapper().writeValueAsString(environmentModel), - MEDIATYPE_JSON); - - client.updateEnvironment(); - assertNotNull(client.getEnvironment()); - assertEquals(client.getEnvironment(), environmentModel); - } - - @Test - public void testClient_flagsApiException() - throws FlagsmithApiError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, () -> client.getEnvironmentFlags()); - } - - @Test - public void testClient_flagsApiEmpty() - throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - "[]", - MEDIATYPE_JSON); - - assertNotNull(client); - List flags = client.getEnvironmentFlags().getAllFlags(); - assertTrue(flags.isEmpty()); - } - - @Test - public void testClient_flagsApi() - throws JsonProcessingException, FlagsmithClientError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - List featureStateModel = FlagsmithTestHelper.getFlags(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - MapperFactory.getMapper().writeValueAsString(featureStateModel), - MEDIATYPE_JSON); - - List flags = client.getEnvironmentFlags().getAllFlags(); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - @Test - public void testClient_identityFlagsApiNoTraitsException() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .post(baseUrl + "/identities/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, () -> client.getIdentityFlags(identifier)); - } - - @Test - public void testClient_identityFlagsApiNoTraits() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - String json = FlagsmithTestHelper.getIdentitiesFlags(); - - interceptor.addRule() - .post(baseUrl + "/identities/") - .respond( - json, - MEDIATYPE_JSON); - - List flags = client.getIdentityFlags(identifier).getAllFlags(); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - private static Stream dataProviderForIdentityFlagsApiWithTraitsTest() { - return Stream.of( - Arguments.of( - "identifier", - false, - new HashMap() { - { - put("some_trait", "some_value"); - put("transient_trait", new TraitConfig("transient_value", true)); - } - }, FlagsmithTestHelper.getIdentityRequest("identifier", new ArrayList() { - { - add( - SdkTraitModel.builder() - .traitKey("some_trait") - .traitValue("some_value") - .build() - ); - add( - SdkTraitModel.builder() - .traitKey("transient_trait") - .traitValue("transient_value") - .isTransient(true) - .build() - ); + private static String DEFAULT_FLAG_VALUE = "foobar"; + private static boolean DEFAULT_FLAG_STATE = true; + + private static BaseFlag defaultHandler(String featureName) { + DefaultFlag defaultFlag = new DefaultFlag(); + defaultFlag.setEnabled(DEFAULT_FLAG_STATE); + defaultFlag.setValue(DEFAULT_FLAG_VALUE); + defaultFlag.setFeatureName(featureName); + return defaultFlag; + } + + @Test + public void testClient_When_Cache_Disabled_Return_Null() { + FlagsmithClient client = FlagsmithClient.newBuilder() + .setApiKey("api-key") + .build(); + + FlagsmithCache cache = client.getCache(); + + assertNull(cache); + } + + @Test + public void testClient_validateObjectCreation() throws InterruptedException { + PollingManager manager = mock(PollingManager.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withPollingManager(manager) + .withConfiguration( + FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) + .setApiKey("ser.abcdefg") + .build(); + + Thread.sleep(10); + verify(manager, times(1)).startPolling(); + } + + @Test + public void testLocalEvaluationRequiresServerKey() throws InterruptedException { + assertThrows(RuntimeException.class, () -> FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) + .setApiKey("not-a-server-key") + .build()); + } + + @Test + public void testClient_errorEnvironmentApi() { + Logger logger = mock(Logger.class); + + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .enableLogging(logger) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + client.updateEnvironment(); + + // Verify that an error was written to the log by mocking the logger and + // checking that a call was made + // with the expected log message. Note that the logger will also have other + // invocations so we need to + // iterate over them to check that the one we expect has been made. + boolean found = false; + String expectedMsg = "Unable to update environment from API. No environment configured - using defaultHandler if configured."; + for (Invocation invocation : Mockito.mockingDetails(logger).getInvocations().stream() + .collect(Collectors.toList())) { + if (invocation.getArgument(0).toString().contains(expectedMsg)) { + found = true; + } } - })), - Arguments.of( - "transient-identifier", - true, - new HashMap() { - { - put("some_trait", "some_value"); + assertTrue(found); + } + + @Test + public void testClient_validateEnvironment() + throws JsonProcessingException { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + FlagsmithTestHelper.environmentString(), + MEDIATYPE_JSON); + + client.updateEnvironment(); + assertNotNull(client.getEvaluationContext()); + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testClient_flagsApiException() + throws FlagsmithApiError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, () -> client.getEnvironmentFlags()); + } + + @Test + public void testClient_flagsApiEmpty() + throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + "[]", + MEDIATYPE_JSON); + + assertNotNull(client); + List flags = client.getEnvironmentFlags().getAllFlags(); + assertTrue(flags.isEmpty()); + } + + @Test + public void testClient_flagsApi() + throws JsonProcessingException, FlagsmithClientError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + List featureStateModel = FlagsmithTestHelper.getFlags(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + MapperFactory.getMapper().writeValueAsString(featureStateModel), + MEDIATYPE_JSON); + + List flags = client.getEnvironmentFlags().getAllFlags(); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + @Test + public void testClient_identityFlagsApiNoTraitsException() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .post(baseUrl + "/identities/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, () -> client.getIdentityFlags(identifier)); + } + + @Test + public void testClient_identityFlagsApiNoTraits() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + String json = FlagsmithTestHelper.getIdentitiesFlags(); + + interceptor.addRule() + .post(baseUrl + "/identities/") + .respond( + json, + MEDIATYPE_JSON); + + List flags = client.getIdentityFlags(identifier).getAllFlags(); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + private static Stream dataProviderForIdentityFlagsApiWithTraitsTest() { + return Stream.of( + Arguments.of( + "identifier", + false, + new HashMap() { + { + put("some_trait", "some_value"); + put("transient_trait", new TraitConfig( + "transient_value", true)); + } + }, FlagsmithTestHelper.getIdentityRequest("identifier", + new ArrayList() { + { + add( + SdkTraitModel.builder() + .traitKey("some_trait") + .traitValue("some_value") + .build()); + add( + SdkTraitModel.builder() + .traitKey("transient_trait") + .traitValue("transient_value") + .isTransient(true) + .build()); + } + })), + Arguments.of( + "transient-identifier", + true, + new HashMap() { + { + put("some_trait", "some_value"); + } + }, FlagsmithTestHelper.getIdentityRequest("transient-identifier", + new ArrayList() { + { + add( + TraitModel.builder() + .traitKey("some_trait") + .traitValue("some_value") + .build()); + } + }, true))); + } + + @ParameterizedTest + @MethodSource("dataProviderForIdentityFlagsApiWithTraitsTest") + public void testClient_identityFlagsApiWithTraits( + String identifier, boolean isTransient, Map traits, JsonNode expectedRequest) + throws FlagsmithClientError, IOException { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + // mocking the requestor + ((FlagsmithApiWrapper) client.getFlagsmithSdk()).setRequestor(requestProcessor); + String json = FlagsmithTestHelper.getIdentitiesFlags(); + TypeReference tr = new TypeReference() { + }; + + when(requestProcessor.executeAsync(any(), any(), any())) + .thenReturn( + FlagsmithTestHelper.futurableReturn( + MapperFactory.getMapper().readValue(json, tr))); + + List flags = client.getIdentityFlags(identifier, traits, isTransient).getAllFlags(); + + ArgumentCaptor argument = ArgumentCaptor.forClass(Request.class); + verify(requestProcessor, times(1)).executeAsync(argument.capture(), any(), any()); + + Buffer buffer = new Buffer(); + argument.getValue().body().writeTo(buffer); + + assertEquals(expectedRequest.toString(), buffer.readUtf8()); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + @Test + public void testClient_identityFlagsApiWithTraitsWithLocalEnvironment() { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("some_trait", "some_value"); + } + }; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/").anyTimes() + .respond(500, ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, + () -> client.getEnvironmentFlags()); + } + + @Test + public void testClient_defaultFlagWithNoEnvironment() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("some_trait", "some_value"); + } + }; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .setDefaultFlagValueFunction((name) -> { + DefaultFlag flag = new DefaultFlag(); + flag.setValue("some-value"); + flag.setEnabled(true); + + return flag; + }) + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + "[]", + MEDIATYPE_JSON); + + Flags flags = client.getEnvironmentFlags(); + + DefaultFlag flag = (DefaultFlag) flags.getFlag("some_feature"); + assertEquals(flag.getIsDefault(), Boolean.TRUE); + assertEquals(flag.getEnabled(), Boolean.TRUE); + assertEquals(flag.getValue(), "some-value"); + } + + @Test + public void testClient_When_Cache_Enabled_Return_Cache_Obj() { + FlagsmithClient client = FlagsmithClient.newBuilder() + .setApiKey("api-key") + .withCache(FlagsmithCacheConfig + .newBuilder() + .enableEnvLevelCaching("newkey-random-name") + .maxSize(2) + .build()) + .build(); + + FlagsmithCache cache = client.getCache(); + + assertNotNull(cache); + } + + @Test + public void testGetIdentitySegmentsNoTraits() throws JsonProcessingException, + FlagsmithClientError { + String baseUrl = "http://bad-url"; + + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + FlagsmithTestHelper.environmentString(), + MEDIATYPE_JSON); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .withLocalEvaluation(true) + .build()) + .setApiKey("ser.abcdefg") + .build(); + + client.updateEnvironment(); + + String identifier = "identifier"; + List segments = client.getIdentitySegments(identifier); + + assertTrue(segments.isEmpty()); + } + + @Test + public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingException, + FlagsmithClientError { + String baseUrl = "http://bad-url"; + + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + FlagsmithTestHelper.environmentString(), + MEDIATYPE_JSON); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .withLocalEvaluation(true) + .build()) + .setApiKey("ser.abcdefg") + .build(); + + client.updateEnvironment(); + + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("foo", "bar"); + } + }; + + List segments = client.getIdentitySegments(identifier, traits); + + assertEquals(segments.size(), 1); + assertEquals(segments.get(0).getName(), "Test segment"); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenThrow(RuntimeException.class); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + // we call the update environment method twice (1st should be successful, 2nd + // will do nothing because of error) + client.updateEnvironment(); + client.updateEnvironment(); + + // Then + // No exception is thrown and the client environment remains what was first + // retrieved from the ApiWrapper + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentExists() { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + // we call the update environment method twice + // (1st should be successful, 2nd will do nothing because of null return) + client.updateEnvironment(); + client.updateEnvironment(); + + // Then + // The client environment is not overwritten with null + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentNotExists() { + // Given + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.updateEnvironment(); + + // Then + // The environment remains null + assertEquals(client.getEvaluationContext(), null); + } + + @Test + public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() + throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()).thenReturn(evaluationContext); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.updateEnvironment(); + + // Then + // Identity overrides are correctly stored + assertEquals( + client.getIdentityFlags("overridden-identity") + .getFlag("some_feature").getValue(), + "overridden-value"); + } + + @Test + public void testClose_StopsPollingManager() { + // Given + PollingManager mockedPollingManager = mock(PollingManager.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withPollingManager(mockedPollingManager) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + verify(mockedPollingManager, times(1)).stopPolling(); + } + + @Test + public void testClose_ClosesFlagsmithSdk() { + // Given + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + verify(mockedApiWrapper, times(1)).close(); + } + + @Test + public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClientError { + // Specific test to ensure that results are consistent when making multiple + // calls to + // evaluate flags soon after the client is instantiated. + + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + // When + // make 3 calls to get identity flags + List results = new ArrayList<>(); + for (int i = 0; i < 3; ++i) { + results.add(client.getIdentityFlags("some-identity")); } - }, FlagsmithTestHelper.getIdentityRequest("transient-identifier", new ArrayList() { - { - add( - TraitModel.builder() - .traitKey("some_trait") - .traitValue("some_value") - .build() - ); + + // Then + // iterate over the results list and verify that the results are all the same + boolean expectedState = true; + String expectedValue = "some-value"; + + for (Flags flags : results) { + assertEquals(flags.isFeatureEnabled("some_feature"), expectedState); + assertEquals(flags.getFeatureValue("some_feature"), expectedValue); } - }, true)) - ); - } - - @ParameterizedTest - @MethodSource("dataProviderForIdentityFlagsApiWithTraitsTest") - public void testClient_identityFlagsApiWithTraits( - String identifier, boolean isTransient, Map traits, JsonNode expectedRequest) - throws FlagsmithClientError, IOException { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - // mocking the requestor - ((FlagsmithApiWrapper) client.getFlagsmithSdk()).setRequestor(requestProcessor); - String json = FlagsmithTestHelper.getIdentitiesFlags(); - TypeReference tr = new TypeReference() { - }; - - when(requestProcessor.executeAsync(any(), any(), any())) - .thenReturn( - FlagsmithTestHelper.futurableReturn(MapperFactory.getMapper().readValue(json, tr))); - - List flags = client.getIdentityFlags(identifier, traits, isTransient).getAllFlags(); - - ArgumentCaptor argument = ArgumentCaptor.forClass(Request.class); - verify(requestProcessor, times(1)).executeAsync(argument.capture(), any(), any()); - - Buffer buffer = new Buffer(); - argument.getValue().body().writeTo(buffer); - - assertEquals(expectedRequest.toString(), buffer.readUtf8()); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - @Test - public void testClient_identityFlagsApiWithTraitsWithLocalEnvironment() { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("some_trait", "some_value"); - } - }; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/").anyTimes() - .respond(500, ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, - () -> client.getEnvironmentFlags()); - } - - @Test - public void testClient_defaultFlagWithNoEnvironment() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("some_trait", "some_value"); - } - }; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .setDefaultFlagValueFunction((name) -> { - DefaultFlag flag = new DefaultFlag(); - flag.setValue("some-value"); - flag.setEnabled(true); - - return flag; - }) - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - "[]", - MEDIATYPE_JSON); - - Flags flags = client.getEnvironmentFlags(); - - DefaultFlag flag = (DefaultFlag) flags.getFlag("some_feature"); - assertEquals(flag.getIsDefault(), Boolean.TRUE); - assertEquals(flag.getEnabled(), Boolean.TRUE); - assertEquals(flag.getValue(), "some-value"); - } - - @Test - public void testClient_When_Cache_Enabled_Return_Cache_Obj() { - FlagsmithClient client = FlagsmithClient.newBuilder() - .setApiKey("api-key") - .withCache(FlagsmithCacheConfig - .newBuilder() - .enableEnvLevelCaching("newkey-random-name") - .maxSize(2) - .build()) - .build(); - - FlagsmithCache cache = client.getCache(); - - assertNotNull(cache); - } - - @Test - public void testGetIdentitySegmentsNoTraits() throws JsonProcessingException, - FlagsmithClientError { - String baseUrl = "http://bad-url"; - - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - MapperFactory.getMapper().writeValueAsString(environmentModel), - MEDIATYPE_JSON); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) + } + + @Test + public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + Flags flagsWithoutOverride = client.getIdentityFlags("test"); + + // When + Flags flagsWithOverride = client.getIdentityFlags("overridden-identity"); + + // Then + assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value"); + assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value"); + } + + @Test + public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() + throws FlagsmithClientError { + // Given + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) + .build(); + + // When + Flags environmentFlags = client.getEnvironmentFlags(); + + // Then + assertEquals(environmentFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); + assertEquals(environmentFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); + } + + @Test + public void testGetIdentityFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() + throws FlagsmithClientError { + // Given + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) + .build(); + + // When + Flags identityFlags = client.getIdentityFlags("some-identity"); + + // Then + assertEquals(identityFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); + assertEquals(identityFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); + } + + @Test + public void testClose() throws FlagsmithApiError, InterruptedException { + // Given + int pollingIntervalSeconds = 1; + + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withLocalEvaluation(true) + .withEnvironmentRefreshIntervalSeconds(pollingIntervalSeconds) + .build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenReturn(FlagsmithTestHelper.evaluationContext()); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + // Since the thread will only stop once it reads the interrupt signal correctly + // on its next polling interval, we need to wait for the polling interval + // to complete before checking the thread has been killed correctly. + Thread.sleep((pollingIntervalSeconds * 1000) + 100); + assertFalse(client.getPollingManager().getIsThreadAlive()); + } + + @Test + public void testOfflineMode() throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineMode(true) + .withOfflineHandler(new DummyOfflineHandler()) + .build(); + + // When + FlagsmithClient client = FlagsmithClient.newBuilder().withConfiguration(config).build(); + + // Then + assertEquals(evaluationContext, client.getEvaluationContext()); + + Flags environmentFlags = client.getEnvironmentFlags(); + assertTrue(environmentFlags.isFeatureEnabled("some_feature")); + + Flags identityFlags = client.getIdentityFlags("my-identity"); + assertTrue(identityFlags.isFeatureEnabled("some_feature")); + } + + @Test + public void testCannotUserOfflineModeWithoutOfflineHandler() throws FlagsmithRuntimeError { + FlagsmithConfig config = FlagsmithConfig.newBuilder().withOfflineMode(true).build(); + + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); + + assertEquals("Offline handler must be provided to use offline mode.", ex.getMessage()); + } + + @Test + public void testCannotUserOfflineHandlerWithLocalEvaluationMode() throws FlagsmithRuntimeError { + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineHandler(new DummyOfflineHandler()) .withLocalEvaluation(true) - .build()) - .setApiKey("ser.abcdefg") - .build(); + .build(); - client.updateEnvironment(); + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); - String identifier = "identifier"; - List segments = client.getIdentitySegments(identifier); + assertEquals("Local evaluation and offline handler cannot be used together.", ex.getMessage()); + } - assertTrue(segments.isEmpty()); - } + @Test + public void testCannotUseDefaultHandlerAndOfflineHandler() throws FlagsmithClientError { + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineHandler(new DummyOfflineHandler()) + .build(); - @Test - public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingException, - FlagsmithClientError { - String baseUrl = "http://bad-url"; + FlagsmithClient.Builder clientBuilder = FlagsmithClient + .newBuilder() + .withConfiguration(config) + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler); - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> clientBuilder.build()); - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - MapperFactory.getMapper().writeValueAsString(environmentModel), - MEDIATYPE_JSON); + assertEquals("Cannot use both default flag handler and offline handler.", ex.getMessage()); + } - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() + @Test + public void testFlagsmithUsesOfflineHandlerIfSetAndNoAPIResponse() throws FlagsmithClientError { + // Given + MockInterceptor interceptor = new MockInterceptor(); + String baseUrl = "http://bad-url"; + + FlagsmithConfig config = FlagsmithConfig + .newBuilder() .baseUri(baseUrl) .addHttpInterceptor(interceptor) - .withLocalEvaluation(true) - .build()) - .setApiKey("ser.abcdefg") - .build(); - - client.updateEnvironment(); - - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("foo", "bar"); - } - }; - - List segments = client.getIdentitySegments(identifier, traits); - - assertEquals(segments.size(), 1); - assertEquals(segments.get(0).getName(), "Test segment"); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()) - .thenReturn(environmentModel) - .thenThrow(RuntimeException.class); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - // we call the update environment method twice (1st should be successful, 2nd - // will do nothing because of error) - client.updateEnvironment(); - client.updateEnvironment(); - - // Then - // No exception is thrown and the client environment remains what was first - // retrieved from the ApiWrapper - assertEquals(client.getEnvironment(), environmentModel); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentExists() { - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()) - .thenReturn(environmentModel) - .thenReturn(null); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - // we call the update environment method twice - // (1st should be successful, 2nd will do nothing because of null return) - client.updateEnvironment(); - client.updateEnvironment(); - - // Then - // The client environment is not overwritten with null - assertEquals(client.getEnvironment(), environmentModel); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentNotExists() { - // Given - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.updateEnvironment(); - - // Then - // The environment remains null - assertEquals(client.getEnvironment(), null); - } - - @Test - public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() { - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()).thenReturn(environmentModel); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.updateEnvironment(); - - // Then - // Identity overrides are correctly stored - IdentityModel actualIdentity = client.getIdentitiesWithOverridesByIdentifier().get("overridden-identity"); - - assertEquals(actualIdentity.getIdentityFeatures().size(), 1); - assertEquals(actualIdentity.getIdentityFeatures().iterator().next().getValue(), "overridden-value"); - } - - @Test - public void testClose_StopsPollingManager() { - // Given - PollingManager mockedPollingManager = mock(PollingManager.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withPollingManager(mockedPollingManager) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - verify(mockedPollingManager, times(1)).stopPolling(); - } - - @Test - public void testClose_ClosesFlagsmithSdk() { - // Given - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - verify(mockedApiWrapper, times(1)).close(); - } - - @Test - public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClientError { - // Specific test to ensure that results are consistent when making multiple - // calls to - // evaluate flags soon after the client is instantiated. - - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()) - .thenReturn(environmentModel) - .thenReturn(null); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - // When - // make 3 calls to get identity flags - List results = new ArrayList<>(); - for (int i = 0; i < 3; ++i) { - results.add(client.getIdentityFlags("some-identity")); - } - - // Then - // iterate over the results list and verify that the results are all the same - boolean expectedState = true; - String expectedValue = "some-value"; - - for (Flags flags : results) { - assertEquals(flags.isFeatureEnabled("some_feature"), expectedState); - assertEquals(flags.getFeatureValue("some_feature"), expectedValue); - } - } - - @Test - public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()) - .thenReturn(environmentModel) - .thenReturn(null); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - Flags flagsWithoutOverride = client.getIdentityFlags("test"); - - // When - Flags flagsWithOverride = client.getIdentityFlags("overridden-identity"); - - // Then - assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value"); - assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value"); - } - - @Test - public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() - throws FlagsmithClientError { - // Given - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) - .build(); - - // When - Flags environmentFlags = client.getEnvironmentFlags(); - - // Then - assertEquals(environmentFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); - assertEquals(environmentFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); - } - - @Test - public void testGetIdentityFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() throws FlagsmithClientError { - // Given - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) - .build(); - - // When - Flags identityFlags = client.getIdentityFlags("some-identity"); - - // Then - assertEquals(identityFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); - assertEquals(identityFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); - } - - @Test - public void testClose() throws FlagsmithApiError, InterruptedException { - // Given - int pollingIntervalSeconds = 1; - - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withLocalEvaluation(true) - .withEnvironmentRefreshIntervalSeconds(pollingIntervalSeconds) - .build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenReturn(FlagsmithTestHelper.environmentModel()); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - // Since the thread will only stop once it reads the interrupt signal correctly - // on its next polling interval, we need to wait for the polling interval - // to complete before checking the thread has been killed correctly. - Thread.sleep((pollingIntervalSeconds * 1000) + 100); - assertFalse(client.getPollingManager().getIsThreadAlive()); - } - - @Test - public void testOfflineMode() throws FlagsmithClientError { - // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineMode(true) - .withOfflineHandler(new DummyOfflineHandler()) - .build(); - - // When - FlagsmithClient client = FlagsmithClient.newBuilder().withConfiguration(config).build(); - - // Then - assertEquals(environmentModel, client.getEnvironment()); - - Flags environmentFlags = client.getEnvironmentFlags(); - assertTrue(environmentFlags.isFeatureEnabled("some_feature")); - - Flags identityFlags = client.getIdentityFlags("my-identity"); - assertTrue(identityFlags.isFeatureEnabled("some_feature")); - } - - @Test - public void testCannotUserOfflineModeWithoutOfflineHandler() throws FlagsmithRuntimeError { - FlagsmithConfig config = FlagsmithConfig.newBuilder().withOfflineMode(true).build(); - - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); - - assertEquals("Offline handler must be provided to use offline mode.", ex.getMessage()); - } - - @Test - public void testCannotUserOfflineHandlerWithLocalEvaluationMode() throws FlagsmithRuntimeError { - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineHandler(new DummyOfflineHandler()) - .withLocalEvaluation(true) - .build(); - - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); - - assertEquals("Local evaluation and offline handler cannot be used together.", ex.getMessage()); - } - - @Test - public void testCannotUseDefaultHandlerAndOfflineHandler() throws FlagsmithClientError { - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineHandler(new DummyOfflineHandler()) - .build(); - - FlagsmithClient.Builder clientBuilder = FlagsmithClient - .newBuilder() - .withConfiguration(config) - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler); - - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> clientBuilder.build()); - - assertEquals("Cannot use both default flag handler and offline handler.", ex.getMessage()); - } - - @Test - public void testFlagsmithUsesOfflineHandlerIfSetAndNoAPIResponse() throws FlagsmithClientError { - // Given - MockInterceptor interceptor = new MockInterceptor(); - String baseUrl = "http://bad-url"; - - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .withOfflineHandler(new DummyOfflineHandler()) - .build(); - FlagsmithClient client = FlagsmithClient - .newBuilder() - .withConfiguration(config) - .setApiKey("some-key") - .build(); - - interceptor.addRule().get(baseUrl + "/flags/").respond(500); - interceptor.addRule().post(baseUrl + "/identities/").respond(500); - - // When - Flags environmentFlags = client.getEnvironmentFlags(); - Flags identityFlags = client.getIdentityFlags("some-identity"); - - // Then - assertTrue(environmentFlags.isFeatureEnabled("some_feature")); - assertTrue(identityFlags.isFeatureEnabled("some_feature")); - } + .withOfflineHandler(new DummyOfflineHandler()) + .build(); + FlagsmithClient client = FlagsmithClient + .newBuilder() + .withConfiguration(config) + .setApiKey("some-key") + .build(); + + interceptor.addRule().get(baseUrl + "/flags/").respond(500); + interceptor.addRule().post(baseUrl + "/identities/").respond(500); + + // When + Flags environmentFlags = client.getEnvironmentFlags(); + Flags identityFlags = client.getIdentityFlags("some-identity"); + + // Then + assertTrue(environmentFlags.isFeatureEnabled("some_feature")); + assertTrue(identityFlags.isFeatureEnabled("some_feature")); + } } diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index bc606879..f16d451a 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -7,13 +7,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.flagsmith.config.FlagsmithCacheConfig; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.BaseFlag; +import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flag; +import com.flagsmith.models.TraitModel; import com.google.common.collect.ImmutableMap; import io.restassured.RestAssured; import io.restassured.http.Header; @@ -84,8 +83,7 @@ public static int createUserIdentity(String userIdentity, String environmentApiK return RestAssured.given() .body(ImmutableMap.of( "identifier", userIdentity, - "environment", environmentApiKey - )) + "environment", environmentApiKey)) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/identities/", environmentApiKey) .then() @@ -99,8 +97,7 @@ public static Map createEnvironment(String name, int projectId) return RestAssured.given() .body(ImmutableMap.of( "name", name, - "project", projectId - )) + "project", projectId)) .headers(defaultHeaders()) .post("/api/v1/environments/") .then() @@ -124,8 +121,7 @@ public static void switchFlag(int featureId, boolean enabled, String apiKey) { RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId - )) + "feature", featureId)) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/featurestates/", apiKey) .then() @@ -141,8 +137,7 @@ public static void switchFlag(int featureId, boolean enabled, String apiKey) { RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId - )) + "feature", featureId)) .headers(defaultHeaders()) .put("/api/v1/environments/{apiKey}/featurestates/{featureStateId}/", apiKey, featureStateId) @@ -171,8 +166,7 @@ public static void switchFlagForUser(int featureId, int userIdentityId, boolean RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId - )) + "feature", featureId)) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/identities/{identityId}/featurestates/", apiKey, userIdentityId) @@ -193,8 +187,7 @@ public static void switchFlagForUser(int featureId, int userIdentityId, boolean RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId - )) + "feature", featureId)) .headers(defaultHeaders()) .put( "/api/v1/environments/{apiKey}/identities/{identityId}/featurestates/{featureStateId}/", @@ -214,8 +207,7 @@ public static void assignTraitToUserIdentity(String userIdentifier, String trait .body(ImmutableMap.of( "identity", ImmutableMap.of("identifier", userIdentifier), "trait_key", traitKey, - "trait_value", traitValue - )) + "trait_value", traitValue)) .headers(defaultHeaders()) .header("x-environment-key", apiKey) .post("/api/v1/traits/") @@ -227,8 +219,7 @@ public static int createProject(String name, int organisationId) { return RestAssured.given() .body(ImmutableMap.of( "name", name, - "organisation", organisationId - )) + "organisation", organisationId)) .headers(defaultHeaders()) .post("/api/v1/projects/") .then() @@ -239,16 +230,15 @@ public static int createProject(String name, int organisationId) { } public static BaseFlag flag( - String name, String description, String type, boolean enabled, String value - ) { - final FeatureModel feature = new FeatureModel(); - feature.setName(name); - feature.setType(type); - + String name, String description, String type, boolean enabled, String value) { final FeatureStateModel result = new FeatureStateModel(); - result.setFeature(feature); result.setEnabled(enabled); result.setValue(value); + + final FeatureStateModel.FeatureModel feature = result.new FeatureModel(); + feature.setName(name); + feature.setType(type); + return Flag.fromFeatureStateModel(result, null); } @@ -267,121 +257,90 @@ public static TraitModel trait(String userIdentifier, String key, String value) return result; } - public static IdentityModel featureUser(String identifier) { - final IdentityModel user = new IdentityModel(); - user.setIdentifier(identifier); - return user; - } - - public static IdentityModel identityOverride() { - final FeatureModel overriddenFeature = new FeatureModel(); - overriddenFeature.setId(1); - overriddenFeature.setName("some_feature"); - overriddenFeature.setType("STANDARD"); - - final FeatureStateModel overriddenFeatureState = new FeatureStateModel(); - overriddenFeatureState.setFeature(overriddenFeature); - overriddenFeatureState.setFeaturestateUuid("d5d0767b-6287-4bb4-9d53-8b87e5458642"); - overriddenFeatureState.setValue("overridden-value"); - overriddenFeatureState.setEnabled(true); - overriddenFeatureState.setMultivariateFeatureStateValues(new ArrayList<>()); - - List identityFeatures = new ArrayList<>(); - identityFeatures.add(overriddenFeatureState); - - final IdentityModel identity = new IdentityModel(); - identity.setIdentifier("overridden-identity"); - identity.setIdentityUuid("65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb"); - identity.setCompositeKey("B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity"); - identity.setEnvironmentApiKey("B62qaMZNwfiqT76p38ggrQ"); - identity.setIdentityFeatures(identityFeatures); - return identity; - } - public static String environmentString() { return "{\n" + - " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + - " \"project\": {\n" + - " \"name\": \"Test project\",\n" + - " \"organisation\": {\n" + - " \"feature_analytics\": false,\n" + - " \"name\": \"Test Org\",\n" + - " \"id\": 1,\n" + - " \"persist_trait_data\": true,\n" + - " \"stop_serving_flags\": false\n" + - " },\n" + - " \"id\": 1,\n" + - " \"hide_disabled_flags\": false,\n" + - " \"segments\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"name\": \"Test segment\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [],\n" + - " \"conditions\": [\n" + - " {\n" + - " \"operator\": \"EQUAL\",\n" + - " \"property_\": \"foo\",\n" + - " \"value\": \"bar\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"segment_overrides\": [],\n" + - " \"id\": 1,\n" + - " \"feature_states\": [\n" + - " {\n" + - " \"multivariate_feature_state_values\": [],\n" + - " \"feature_state_value\": \"some-value\",\n" + - " \"id\": 1,\n" + - " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + - " \"feature\": {\n" + - " \"name\": \"some_feature\",\n" + - " \"type\": \"STANDARD\",\n" + - " \"id\": 1\n" + - " },\n" + - " \"segment_id\": null,\n" + - " \"enabled\": true\n" + - " }\n" + - " ],\n" + - " \"identity_overrides\": [\n" + - " {\n" + - " \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" + - " \"identifier\": \"overridden-identity\",\n" + - " \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" + - " \"identity_features\": [\n" + - " {\n" + - " \"feature_state_value\": \"overridden-value\",\n" + - " \"multivariate_feature_state_values\": [],\n" + - " \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" + - " \"feature\": {\n" + - " \"name\": \"some_feature\",\n" + - " \"type\": \"STANDARD\",\n" + - " \"id\": 1\n" + - " },\n" + - " \"enabled\": true\n" + - " }\n" + - " ],\n" + - " \"identity_traits\": [],\n" + - " \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" + - " }\n" + - " ]\n" + - "}"; + " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + + " \"project\": {\n" + + " \"name\": \"Test project\",\n" + + " \"organisation\": {\n" + + " \"feature_analytics\": false,\n" + + " \"name\": \"Test Org\",\n" + + " \"id\": 1,\n" + + " \"persist_trait_data\": true,\n" + + " \"stop_serving_flags\": false\n" + + " },\n" + + " \"id\": 1,\n" + + " \"hide_disabled_flags\": false,\n" + + " \"segments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"name\": \"Test segment\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [],\n" + + " \"conditions\": [\n" + + " {\n" + + " \"operator\": \"EQUAL\",\n" + + " \"property_\": \"foo\",\n" + + " \"value\": \"bar\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"segment_overrides\": [],\n" + + " \"id\": 1,\n" + + " \"feature_states\": [\n" + + " {\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"feature_state_value\": \"some-value\",\n" + + " \"id\": 1,\n" + + " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"segment_id\": null,\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_overrides\": [\n" + + " {\n" + + " \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" + + " \"identifier\": \"overridden-identity\",\n" + + " \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" + + " \"identity_features\": [\n" + + " {\n" + + " \"feature_state_value\": \"overridden-value\",\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_traits\": [],\n" + + " \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" + + " }\n" + + " ]\n" + + "}"; } - public static EnvironmentModel environmentModel() { + public static EvaluationContext evaluationContext() { try { - return EnvironmentModel.load(MapperFactory.getMapper().readTree(environmentString()), EnvironmentModel.class); + return EngineMappers.mapEnvironmentDocumentToContext(MapperFactory.getMapper().readTree(environmentString())); } catch (JsonProcessingException e) { // environment model json } @@ -414,8 +373,8 @@ public static List getFlags() { try { return MapperFactory.getMapper().readValue( featureJson, - new TypeReference>() {} - ); + new TypeReference>() { + }); } catch (JsonProcessingException e) { e.printStackTrace(); // environment model json @@ -472,11 +431,11 @@ public static JsonNode getIdentityRequest(String identifier, List traits, boolean isTransient) { + String identifier, List traits, boolean isTransient) { final ObjectNode flagsAndTraits = MapperFactory.getMapper().createObjectNode(); flagsAndTraits.putPOJO("identifier", identifier); flagsAndTraits.put("transient", isTransient); - flagsAndTraits.putPOJO("traits", traits != null ? traits : new ArrayList<>()); + flagsAndTraits.putPOJO("traits", traits != null ? traits : new ArrayList<>()); return flagsAndTraits; } diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index c4c80561..0773ed13 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -1,52 +1,65 @@ package com.flagsmith.flagengine; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.flagsmith.MapperFactory; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.models.ResponseJSON; +import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.Flags; + +import groovy.util.Eval; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.BeforeClass; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; - import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; - +import java.util.stream.StreamSupport; import static org.junit.jupiter.api.Assertions.assertEquals; - public class EngineTest { - private static final String ENVIRONMENT_JSON_FILE_LOCATION = - "src/test/java/com/flagsmith/flagengine/enginetestdata/" + - "data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"; + private static final String ENVIRONMENT_JSON_FILE_LOCATION = "src/test/java/com/flagsmith/flagengine/enginetestdata/" + + + "data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"; + private static ObjectMapper objectMapper = MapperFactory.getMapper(); private static Stream engineTestData() { try { - ObjectMapper objectMapper = MapperFactory.getMapper(); - JsonNode engineTestData = objectMapper. - readTree(new File(ENVIRONMENT_JSON_FILE_LOCATION)); - - JsonNode environmentNode = engineTestData.get("environment"); - EnvironmentModel environmentModel = EnvironmentModel.load(environmentNode, EnvironmentModel.class); + JsonNode engineTestData = objectMapper.readTree(new File(ENVIRONMENT_JSON_FILE_LOCATION)); + JsonNode environmentDocument = engineTestData.get("environment"); JsonNode identitiesAndResponses = engineTestData.get("identities_and_responses"); + EvaluationContext baseEvaluationContext = EngineMappers + .mapEnvironmentDocumentToContext(environmentDocument); + List returnValues = new ArrayList<>(); if (identitiesAndResponses.isArray()) { for (JsonNode identityAndResponse : identitiesAndResponses) { - IdentityModel identityModel = - IdentityModel.load(identityAndResponse.get("identity"), IdentityModel.class); - ResponseJSON expectedResponse = - objectMapper.treeToValue(identityAndResponse.get("response"), ResponseJSON.class); + JsonNode identity = identityAndResponse.get("identity"); + Map traits = Optional.ofNullable(identity.get("identity_traits")) + .filter(JsonNode::isArray) + .map(node -> StreamSupport.stream(node.spliterator(), false) + .filter(trait -> trait.hasNonNull("trait_key")) + .collect(Collectors.toMap( + trait -> trait.get("trait_key").asText(), + trait -> objectMapper.convertValue(trait.get("trait_value"), Object.class)))) + .orElseGet(HashMap::new); - returnValues.add(Arguments.of(identityModel, environmentModel, expectedResponse)); + EvaluationContext evaluationContext = EngineMappers.mapContextAndIdentityDataToContext( + baseEvaluationContext, identity.get("identifier").asText(), traits); + + JsonNode expectedResponse = identityAndResponse.get("response"); + + returnValues.add(Arguments.of(evaluationContext, expectedResponse)); } } @@ -61,34 +74,27 @@ private static Stream engineTestData() { @ParameterizedTest() @MethodSource("engineTestData") - public void testEngine(IdentityModel identity, EnvironmentModel environmentModel, ResponseJSON expectedResponse) { - List featureStates = - Engine.getIdentityFeatureStates(environmentModel, identity); - - List sortedFeatureStates = featureStates - .stream() - .sorted((featureState1, t1) - -> featureState1.getFeature().getName() - .compareTo(t1.getFeature().getName())) - .collect(Collectors.toList()); + public void testEngine(EvaluationContext evaluationContext, JsonNode expectedResponse) { + EvaluationResult evaluationResult = Engine.getEvaluationResult(evaluationContext); - List sortedResponse = expectedResponse.getFlags() - .stream() - .sorted((featureState1, t1) - -> featureState1.getFeature().getName() - .compareTo(t1.getFeature().getName())) - .collect(Collectors.toList()); + List flags = objectMapper.convertValue( + expectedResponse.get("flags"), + new TypeReference>() { + }); - assert (sortedResponse.size() == sortedFeatureStates.size()); + flags.sort((fsm1, fsm2) -> fsm1.getFeature().getName().compareTo(fsm2.getFeature().getName())); + List sortedResults = evaluationResult.getFlags().stream() + .sorted((fr1, fr2) -> fr1.getName().compareTo(fr2.getName())) + .collect(Collectors.toList()); - int index = 0; - for (FeatureStateModel featureState : sortedFeatureStates) { - Object featureStateValue = featureState.getValue(identity.getDjangoId()); - Object expectedResponseValue = sortedResponse.get(index).getValue(identity.getDjangoId()); + assertEquals(flags.size(), sortedResults.size()); + for (int i = 0; i < flags.size(); i++) { + FeatureStateModel fsm = flags.get(i); + FlagResult fr = sortedResults.get(i); - assertEquals(featureStateValue, expectedResponseValue); - assertEquals(featureState.getEnabled(), sortedResponse.get(index).getEnabled()); - index++; + assertEquals(fr.getName(), fsm.getFeature().getName()); + assertEquals(fr.getEnabled(), fsm.getEnabled()); + assertEquals(fr.getValue(), fsm.getValue()); } } -} +} \ No newline at end of file diff --git a/src/test/java/com/flagsmith/flagengine/fixtures/FlagEngineFixtures.java b/src/test/java/com/flagsmith/flagengine/fixtures/FlagEngineFixtures.java deleted file mode 100644 index fbca523a..00000000 --- a/src/test/java/com/flagsmith/flagengine/fixtures/FlagEngineFixtures.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.flagsmith.flagengine.fixtures; - -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.features.MultivariateFeatureOptionModel; -import com.flagsmith.flagengine.features.MultivariateFeatureStateValueModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import com.flagsmith.flagengine.organisations.OrganisationModel; -import com.flagsmith.flagengine.projects.ProjectModel; -import com.flagsmith.flagengine.segments.SegmentConditionModel; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.flagengine.segments.SegmentRuleModel; -import com.flagsmith.flagengine.segments.constants.SegmentConditions; -import com.flagsmith.flagengine.segments.constants.SegmentRules; -import java.util.Arrays; - -public class FlagEngineFixtures { - - public static SegmentConditionModel segmentCondition() { - SegmentConditionModel condition = new SegmentConditionModel(); - condition.setValue("bar"); - condition.setProperty_("bar"); - condition.setOperator(SegmentConditions.EQUAL); - - return condition; - } - - public static SegmentRuleModel segmentRule() { - SegmentRuleModel rule = new SegmentRuleModel(); - rule.setType(SegmentRules.ALL_RULE.name()); - rule.setConditions(Arrays.asList(segmentCondition())); - - return rule; - } - - public static SegmentModel segment() { - SegmentModel segment = new SegmentModel(); - segment.setId(1); - segment.setName("my_segment"); - segment.setRules(Arrays.asList(segmentRule())); - - return segment; - } - - public static OrganisationModel organisation() { - OrganisationModel organisation = new OrganisationModel(); - organisation.setId(1); - organisation.setName("Test Project"); - organisation.setStopServingFlags(Boolean.FALSE); - organisation.setPersistTraitData(Boolean.TRUE); - organisation.setFeatureAnalytics(Boolean.TRUE); - - return organisation; - } - - public static ProjectModel project() { - ProjectModel project = new ProjectModel(); - project.setId(1); - project.setName("Test Project"); - project.setOrganisation(organisation()); - project.setSegments(Arrays.asList(segment())); - project.setHideDisabledFlags(Boolean.FALSE); - - return project; - } - - public static FeatureModel feature1() { - FeatureModel feature = new FeatureModel(); - feature.setId(1); - feature.setName("feature_1"); - feature.setType("STANDARD"); - - return feature; - } - - public static FeatureModel feature2() { - FeatureModel feature = new FeatureModel(); - feature.setId(2); - feature.setName("feature_2"); - feature.setType("STANDARD"); - - return feature; - } - - public static FeatureStateModel featureState1() { - FeatureStateModel feature = new FeatureStateModel(); - feature.setDjangoId(1); - feature.setEnabled(Boolean.TRUE); - feature.setFeature(feature1()); - - return feature; - } - - public static FeatureStateModel featureState2() { - FeatureStateModel feature = new FeatureStateModel(); - feature.setDjangoId(2); - feature.setEnabled(Boolean.FALSE); - feature.setFeature(feature2()); - - return feature; - } - - public static EnvironmentModel environment() { - EnvironmentModel environment = new EnvironmentModel(); - environment.setId(1); - environment.setApiKey("api-key"); - environment.setProject(project()); - environment.setFeatureStates(Arrays.asList(featureState1(), featureState2())); - - return environment; - } - - public static IdentityModel identity() { - IdentityModel identityModel = new IdentityModel(); - identityModel.setEnvironmentApiKey(environment().getApiKey()); - identityModel.setIdentifier("identity_1"); - - return identityModel; - } - - public static TraitModel traitMatchingSegment() { - TraitModel trait = new TraitModel(); - trait.setTraitKey(segmentCondition().getProperty_()); - trait.setTraitValue(segmentCondition().getValue()); - - return trait; - } - - public static IdentityModel identityInSegment() { - IdentityModel identityModel = new IdentityModel(); - identityModel.setEnvironmentApiKey("identity_2"); - identityModel.setEnvironmentApiKey(environment().getApiKey()); - identityModel.setIdentityTraits(Arrays.asList(traitMatchingSegment())); - - return identityModel; - } - - public static FeatureStateModel segmentOverrideFs() { - FeatureStateModel featureState = new FeatureStateModel(); - featureState.setDjangoId(4); - featureState.setFeature(feature1()); - featureState.setEnabled(Boolean.FALSE); - - featureState.setValue("segment_override"); - - return featureState; - } - - public static MultivariateFeatureOptionModel mvFeatureFeatureOption() { - MultivariateFeatureOptionModel multi = new MultivariateFeatureOptionModel(); - multi.setId(1); - multi.setValue("test_value"); - - return multi; - } - - public static MultivariateFeatureStateValueModel mvFeatureStateValue() { - MultivariateFeatureStateValueModel multi = new MultivariateFeatureStateValueModel(); - multi.setId(1); - multi.setMultivariateFeatureOption(mvFeatureFeatureOption()); - multi.setPercentageAllocation(100f); - - return multi; - } - - public static EnvironmentModel environmentWithSegmentOverride() { - EnvironmentModel environmentModel = environment(); - FeatureStateModel segmentOverrideFs = segmentOverrideFs(); - SegmentModel segment = segment(); - - segment.getFeatureStates().add(segmentOverrideFs); - environmentModel.getProject().getSegments().add(segment); - - return environmentModel; - } -} diff --git a/src/test/java/com/flagsmith/flagengine/helpers/FeatureStateHelper.java b/src/test/java/com/flagsmith/flagengine/helpers/FeatureStateHelper.java deleted file mode 100644 index d0eb20e8..00000000 --- a/src/test/java/com/flagsmith/flagengine/helpers/FeatureStateHelper.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.flagsmith.flagengine.helpers; - -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; - -import java.util.List; - -public class FeatureStateHelper { - - public static FeatureStateModel getFeatureStateForFeature(List featureStates, - FeatureModel feature) { - return featureStates - .stream() - .filter((featureState) -> featureState.getFeature().equals(feature)) - .findFirst().orElse(null); - } - - public static FeatureStateModel getFeatureStateForFeatureByName( - List featureStates, String name) { - return featureStates - .stream() - .filter((featureState) -> featureState.getFeature().getName().equals(name)) - .findFirst().orElse(null); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java b/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java index 3001d962..8cf7b89f 100644 --- a/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java +++ b/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java @@ -1,10 +1,9 @@ package com.flagsmith.flagengine.models; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import lombok.Data; - +import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.TraitModel; import java.util.List; +import lombok.Data; @Data public class ResponseJSON { diff --git a/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesModelTest.java deleted file mode 100644 index 08139bfc..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesModelTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.flagsmith.flagengine.unit.Identities; - -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.fixtures.FlagEngineFixtures; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class IdentitiesModelTest { - - @Test - public void testCompositeKey() { - String environmentApiKey = "abc123"; - String identifier = "identity"; - - IdentityModel identity = new IdentityModel(); - identity.setEnvironmentApiKey(environmentApiKey); - identity.setIdentifier(identifier); - - Assertions.assertEquals(identity.getCompositeKey(), environmentApiKey + "_" + identifier); - } - - @Test - public void testIdentityModelCreatesDefaultIdentityUuid() { - String environmentApiKey = "abc123"; - String identifier = "identity"; - - IdentityModel identity = new IdentityModel(); - identity.setEnvironmentApiKey(environmentApiKey); - identity.setIdentifier(identifier); - - Assertions.assertNotNull(identity.getIdentityUuid()); - } - - @Test - public void testUpdateTraitsRemoveTraitsWithNoneValue() { - IdentityModel identity = FlagEngineFixtures.identityInSegment(); - - TraitModel traitToRemove = new TraitModel(); - traitToRemove.setTraitKey(identity.getIdentityTraits().get(0).getTraitKey()); - - identity.updateTraits(Arrays.asList(traitToRemove)); - - Assertions.assertNotNull(identity.getIdentityTraits()); - Assertions.assertEquals(identity.getIdentityTraits().size(), 0); - } - - @Test - public void testUpdateIdentityTraitsUpdatesTraitValue() { - IdentityModel identity = FlagEngineFixtures.identityInSegment(); - - TraitModel traitToUpdate = new TraitModel(); - traitToUpdate.setTraitKey(identity.getIdentityTraits().get(0).getTraitKey()); - traitToUpdate.setTraitValue("updated"); - - identity.updateTraits(Arrays.asList(traitToUpdate)); - - Assertions.assertNotNull(identity.getIdentityTraits()); - Assertions.assertEquals(identity.getIdentityTraits().size(), 1); - Assertions.assertEquals(identity.getIdentityTraits().get(0), traitToUpdate); - } - - @Test - public void testUpdateTraitsAddsNewTraits() { - IdentityModel identity = FlagEngineFixtures.identityInSegment(); - - TraitModel traitToUpdate = new TraitModel(); - traitToUpdate.setTraitKey("new"); - traitToUpdate.setTraitValue("updated"); - - identity.updateTraits(Arrays.asList(traitToUpdate)); - - Assertions.assertNotNull(identity.getIdentityTraits()); - Assertions.assertEquals(identity.getIdentityTraits().size(), 2); - - Boolean isPresent = identity.getIdentityTraits().stream() - .anyMatch((it) -> it.equals(traitToUpdate)); - - Assertions.assertTrue(isPresent); - } - - @Test - public void testAppendFeatureState() { - FeatureStateModel fs1 = FlagEngineFixtures.featureState1(); - fs1.setEnabled(false); - - IdentityModel identity = FlagEngineFixtures.identity(); - identity.getIdentityFeatures().add(fs1); - - Boolean isPresent = identity.getIdentityFeatures().stream() - .anyMatch((fs) -> fs.equals(fs1)); - - Assertions.assertTrue(isPresent); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesTest.java b/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesTest.java deleted file mode 100644 index 979b815c..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/Identities/IdentitiesTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.flagsmith.flagengine.unit.Identities; - -import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.MapperFactory; -import com.flagsmith.flagengine.identities.IdentityModel; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class IdentitiesTest { - - @Test - public void testBuildIdentityModelFromDictionaryNoFeatureStates() throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"identifier\": \"test-identity\",\n" + - " \"environment_api_key\": \"api-key\",\n" + - " \"created_date\": \"2021-08-22T06:25:23.406995Z\",\n" + - " \"identity_traits\": [{\"trait_key\": \"trait_key\", \"trait_value\": \"trait_value\"}]\n" + - "}"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - IdentityModel identityModel = IdentityModel.load(node, IdentityModel.class); - - Assertions.assertNotNull(identityModel.getIdentityFeatures()); - Assertions.assertEquals(identityModel.getIdentityFeatures().size(), 0); - - Assertions.assertNotNull(identityModel.getIdentityTraits()); - Assertions.assertEquals(identityModel.getIdentityTraits().size(), 1); - } - - @Test - public void testBuildIdentityModelFromDictionaryUsesIdentityFeatureListForIdentityFeatures() - throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"identifier\": \"test-identity\",\n" + - " \"environment_api_key\": \"api-key\",\n" + - " \"created_date\": \"2021-08-22T06:25:23.406995Z\",\n" + - " \"identity_features\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"feature\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"test_feature\",\n" + - " \"type\": \"STANDARD\"\n" + - " },\n" + - " \"enabled\": true,\n" + - " \"feature_state_value\": \"some-value\"\n" + - " }\n" + - " ]\n" + - " }"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - IdentityModel identityModel = IdentityModel.load(node, IdentityModel.class); - - Assertions.assertNotNull(identityModel.getIdentityFeatures()); - Assertions.assertEquals(identityModel.getIdentityFeatures().size(), 1); - } - - @Test - public void testBuildBuildIdentityModelFromDictCreatesIdentityUuid() throws Exception { - String json = "{\"identifier\": \"test_user\", \"environment_api_key\": \"some_key\"}"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - IdentityModel identityModel = IdentityModel.load(node, IdentityModel.class); - - Assertions.assertNotNull(identityModel); - Assertions.assertNotNull(identityModel.getIdentityUuid()); - } - - @Test - public void testBuildIdentityModelFromDictionaryWithFeatureStates() throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"identifier\": \"test-identity\",\n" + - " \"environment_api_key\": \"api-key\",\n" + - " \"created_date\": \"2021-08-22T06:25:23.406995Z\",\n" + - " \"identity_features\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"feature\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"test_feature\",\n" + - " \"type\": \"STANDARD\"\n" + - " },\n" + - " \"enabled\": true,\n" + - " \"feature_state_value\": \"some-value\"\n" + - " }\n" + - " ]\n" + - " }"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - IdentityModel identityModel = IdentityModel.load(node, IdentityModel.class); - - Assertions.assertNotNull(identityModel); - Assertions.assertNotNull(identityModel.getIdentityFeatures()); - Assertions.assertEquals(identityModel.getIdentityFeatures().size(), 1); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java b/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java deleted file mode 100644 index 0ff35024..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.flagsmith.flagengine.unit.environments; - -import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.MapperFactory; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.helpers.FeatureStateHelper; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class EnvironmentTest { - - @Test - public void test_get_flags_for_environment_returns_feature_states_for_environment_dictionary() - throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"api_key\": \"api-key\",\n" + - " \"project\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"test project\",\n" + - " \"organisation\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"Test Org\",\n" + - " \"stop_serving_flags\": false,\n" + - " \"persist_trait_data\": true,\n" + - " \"feature_analytics\": true\n" + - " },\n" + - " \"hide_disabled_flags\": false\n" + - " },\n" + - " \"feature_states\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"enabled\": true,\n" + - " \"feature_state_value\": null,\n" + - " \"feature\": {\"id\": 1, \"name\": \"enabled_feature\", \"type\": \"STANDARD\"}\n" + - " },\n" + - " {\n" + - " \"id\": 2,\n" + - " \"enabled\": false,\n" + - " \"feature_state_value\": null,\n" + - " \"feature\": {\"id\": 2, \"name\": \"disabled_feature\", \"type\": \"STANDARD\"}\n" + - " },\n" + - " {\n" + - " \"id\": 3,\n" + - " \"enabled\": true,\n" + - " \"feature_state_value\": \"foo\",\n" + - " \"feature\": {\n" + - " \"id\": 3,\n" + - " \"name\": \"feature_with_string_value\",\n" + - " \"type\": \"STANDARD\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - EnvironmentModel environmentModel = EnvironmentModel.load(node, EnvironmentModel.class); - - Assertions.assertNotNull(environmentModel); - Assertions.assertTrue(environmentModel.getId() == 1); - Assertions.assertEquals(environmentModel.getApiKey(), "api-key"); - - Assertions.assertNotNull(environmentModel.getProject()); - Assertions.assertNotNull(environmentModel.getFeatureStates()); - - Assertions.assertTrue(environmentModel.getFeatureStates().size() == 3); - - FeatureStateModel featureState = FeatureStateHelper.getFeatureStateForFeatureByName( - environmentModel.getFeatureStates(), - "feature_with_string_value" - ); - - Assertions.assertNotNull(featureState); - } - - @Test - public void test_build_environment_model_with_multivariate_flag() throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"api_key\": \"api-key\",\n" + - " \"project\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"test project\",\n" + - " \"organisation\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"Test Org\",\n" + - " \"stop_serving_flags\": false,\n" + - " \"persist_trait_data\": true,\n" + - " \"feature_analytics\": true\n" + - " },\n" + - " \"hide_disabled_flags\": false\n" + - " },\n" + - " \"feature_states\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"enabled\": true,\n" + - " \"feature_state_value\": null,\n" + - " \"feature\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"enabled_feature\",\n" + - " \"type\": \"STANDARD\"\n" + - " },\n" + - " \"multivariate_feature_state_values\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"percentage_allocation\": 10.0,\n" + - " \"multivariate_feature_option\": {\n" + - " \"value\": \"value-1\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"id\": 2,\n" + - " \"percentage_allocation\": 10.0,\n" + - " \"multivariate_feature_option\": {\n" + - " \"value\": \"value-2\",\n" + - " \"id\": 2\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}\n"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - EnvironmentModel environmentModel = EnvironmentModel.load(node, EnvironmentModel.class); - - Assertions.assertNotNull(environmentModel); - - Assertions.assertNotNull(environmentModel.getFeatureStates()); - Assertions.assertEquals(environmentModel.getFeatureStates().size(), 1); - - FeatureStateModel featureState = environmentModel.getFeatureStates().get(0); - Assertions.assertNotNull(featureState.getMultivariateFeatureStateValues()); - Assertions.assertEquals(featureState.getMultivariateFeatureStateValues().size(), 2); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureModelTest.java deleted file mode 100644 index e5034b28..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureModelTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.flagsmith.flagengine.unit.feature; - -import com.flagsmith.MapperFactory; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.features.MultivariateFeatureOptionModel; -import com.flagsmith.flagengine.features.MultivariateFeatureStateValueModel; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.Arrays; -import java.util.stream.Stream; - -public class FeatureModelTest { - - private static String MV_FEATURE_CONTROL_VALUE = "control"; - private static String MV_FEATURE_VALUE_1 = "foo"; - private static String MV_FEATURE_VALUE_2 = "bar"; - - public void featureStateModelShouldNotHaveEmpty() { - FeatureStateModel featureStateModel = new FeatureStateModel(); - featureStateModel.setDjangoId(1234); - featureStateModel.setEnabled(true); - - Assertions.assertNotNull(featureStateModel.getFeaturestateUuid()); - } - - public void testInitializingMultivariateFeatureStateValueCreatesDefaultUuid() { - - MultivariateFeatureOptionModel mvfom = new MultivariateFeatureOptionModel(); - mvfom.setValue("value"); - - MultivariateFeatureStateValueModel mvfsvm = new MultivariateFeatureStateValueModel(); - mvfsvm.setMultivariateFeatureOption(mvfom); - mvfsvm.setId(1); - mvfsvm.setPercentageAllocation(10f); - - Assertions.assertNotNull(mvfsvm.getMvFsValueUuid()); - } - - public void testFeatureStateGetValueNoMvValues() { - FeatureModel feature1 = new FeatureModel(); - feature1.setId(1); - feature1.setName("mv_feature"); - feature1.setType("STANDARD"); - - FeatureStateModel featureState = new FeatureStateModel(); - featureState.setFeature(feature1); - featureState.setDjangoId(1); - featureState.setEnabled(true); - featureState.setValue("foo"); - - Assertions.assertTrue(featureState.getValue().equals("foo")); - Assertions.assertTrue(featureState.getValue(1).equals("foo")); - } - - private static Stream dataProviderForFeatureStateValueTest() { - return Stream.of( - Arguments.of(10f, MV_FEATURE_VALUE_1), - Arguments.of(40f, MV_FEATURE_VALUE_2), - Arguments.of(70f, MV_FEATURE_CONTROL_VALUE) - ); - } - - @ParameterizedTest - @MethodSource("dataProviderForFeatureStateValueTest") - public void testFeatureStateGetValueMvValues(Float percentageValue, String expectedValue) { - FeatureModel feature1 = new FeatureModel(); - feature1.setId(1); - feature1.setName("mv_feature"); - feature1.setType("STANDARD"); - - MultivariateFeatureOptionModel mv1 = new MultivariateFeatureOptionModel(); - mv1.setId(1); - mv1.setValue(MV_FEATURE_VALUE_1); - - MultivariateFeatureOptionModel mv2 = new MultivariateFeatureOptionModel(); - mv2.setId(2); - mv2.setValue(MV_FEATURE_VALUE_2); - - MultivariateFeatureStateValueModel mvf1 = new MultivariateFeatureStateValueModel(); - mvf1.setPercentageAllocation(30f); - mvf1.setId(1); - mvf1.setMultivariateFeatureOption(mv1); - - MultivariateFeatureStateValueModel mvf2 = new MultivariateFeatureStateValueModel(); - mvf2.setPercentageAllocation(30f); - mvf2.setId(1); - mvf2.setMultivariateFeatureOption(mv2); - - FeatureStateModel featureState = new FeatureStateModel(); - featureState.setDjangoId(1); - featureState.setFeature(feature1); - featureState.setEnabled(true); - featureState.setMultivariateFeatureStateValues(Arrays.asList(mvf1, mvf2)); - featureState.setDjangoId(1); - featureState.setValue(MV_FEATURE_CONTROL_VALUE); - - Object value = featureState.getValue(1); - // TODO mock hash method - } - - public void loadMultiVariateFeatureOptionWithoutId() throws Exception { - String json = "{\"value\": 1}"; - MultivariateFeatureOptionModel variate = - MultivariateFeatureOptionModel.load(MapperFactory.getMapper().readTree(json), - MultivariateFeatureOptionModel.class); - Assertions.assertNull(variate.getId()); - } - - public void loadMultiVariateFeatureStateWithoutId() throws Exception { - String json = - "{ \"multivariate_feature_option\":{\"value\": 1},\"percentage_allocation\": 10 }"; - MultivariateFeatureStateValueModel variate = - MultivariateFeatureStateValueModel.load(MapperFactory.getMapper().readTree(json), - MultivariateFeatureStateValueModel.class); - Assertions.assertNull(variate.getId()); - Assertions.assertEquals(variate.getPercentageAllocation(), 10f); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureStateModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureStateModelTest.java deleted file mode 100644 index 51d8907f..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/feature/FeatureStateModelTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.flagsmith.flagengine.unit.feature; - -import com.flagsmith.flagengine.features.FeatureSegmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - - -public class FeatureStateModelTest { - - @Test() - public void testFeatureState_IsHigherPriority_TwoNullFeatureSegments() { - // Given - FeatureStateModel featureState1 = new FeatureStateModel(); - FeatureStateModel featureState2 = new FeatureStateModel(); - - // Then - Assertions.assertFalse(featureState1.isHigherPriority(featureState2)); - Assertions.assertFalse(featureState2.isHigherPriority(featureState1)); - } - - @Test() - public void testFeatureState_IsHigherPriority_OneNullFeatureSegment() { - // Given - FeatureStateModel featureState1 = new FeatureStateModel(); - FeatureStateModel featureState2 = new FeatureStateModel(); - - FeatureSegmentModel featureSegment = new FeatureSegmentModel(1); - featureState1.setFeatureSegment(featureSegment); - - // Then - Assertions.assertTrue(featureState1.isHigherPriority(featureState2)); - Assertions.assertFalse(featureState2.isHigherPriority(featureState1)); - } - - @Test() - public void testFeatureState_IsHigherPriority() { - // Given - FeatureStateModel featureState1 = new FeatureStateModel(); - FeatureStateModel featureState2 = new FeatureStateModel(); - - FeatureSegmentModel featureSegment1 = new FeatureSegmentModel(1); - featureState1.setFeatureSegment(featureSegment1); - - FeatureSegmentModel featureSegment2 = new FeatureSegmentModel(2); - featureState2.setFeatureSegment(featureSegment2); - - // Then - Assertions.assertTrue(featureState1.isHigherPriority(featureState2)); - Assertions.assertFalse(featureState2.isHigherPriority(featureState1)); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/organizations/OrganizationsTest.java b/src/test/java/com/flagsmith/flagengine/unit/organizations/OrganizationsTest.java deleted file mode 100644 index c1846d2d..00000000 --- a/src/test/java/com/flagsmith/flagengine/unit/organizations/OrganizationsTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.flagsmith.flagengine.unit.organizations; - -import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.MapperFactory; -import com.flagsmith.flagengine.organisations.OrganisationModel; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class OrganizationsTest { - - @Test - public void testUniqueSlugProperty() throws Exception { - String json = "{\n" + - " \"id\": 1,\n" + - " \"name\": \"test\",\n" + - " \"feature_analytics\": false,\n" + - " \"stop_serving_flags\": false,\n" + - " \"persist_trait_data\": false\n" + - "}"; - - JsonNode node = MapperFactory.getMapper().readTree(json); - OrganisationModel organisationModel = OrganisationModel.load(node, OrganisationModel.class); - - assertTrue(organisationModel.uniqueSlug().equals("1-test")); - } -} diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java b/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java index 198b3c6e..778c64f4 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java @@ -1,11 +1,10 @@ package com.flagsmith.flagengine.unit.segments; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import com.flagsmith.flagengine.segments.SegmentConditionModel; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.flagengine.segments.SegmentRuleModel; import com.flagsmith.flagengine.segments.constants.SegmentConditions; -import com.flagsmith.flagengine.segments.constants.SegmentRules; +import com.flagsmith.flagengine.SegmentContext; +import com.flagsmith.flagengine.SegmentCondition; +import com.flagsmith.flagengine.SegmentRule; +import com.flagsmith.models.TraitModel; import java.util.ArrayList; import java.util.Arrays; @@ -22,153 +21,105 @@ public class IdentitySegmentFixtures { public static final String traitKey3 = "date_joined"; public static final String traitValue3 = "2021-01-01"; - public static SegmentModel emptySegment() { - SegmentModel segment = new SegmentModel(); - segment.setId(1); - segment.setName("empty_segment"); - - return segment; + public static SegmentContext emptySegment() { + return new SegmentContext().withKey("1").withName("empty_segment"); } - public static SegmentModel segmentSingleCondition() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); - segmentCondition.setValue(traitValue1); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ALL_RULE.getRule()); - segmentRule.setConditions(Arrays.asList(segmentCondition)); - - SegmentModel segment = new SegmentModel(); - segment.setId(2); - segment.setName("segment_one_condition"); - segment.setRules(Arrays.asList(segmentRule)); - - return segment; + public static SegmentContext segmentSingleCondition() { + return new SegmentContext().withKey("2").withName("segment_one_condition") + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey1) + .withValue(traitValue1))))); } - public static SegmentModel segmentMultipleConditionsAll() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); - segmentCondition.setValue(traitValue1); - - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); - segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); - segmentCondition2.setValue(traitValue2); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ALL_RULE.getRule()); - segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - - SegmentModel segment = new SegmentModel(); - segment.setId(3); - segment.setName("segment_multiple_conditions_all"); - segment.setRules(Arrays.asList(segmentRule)); - - return segment; + public static SegmentContext segmentMultipleConditionsAll() { + return new SegmentContext().withKey("3").withName("segment_multiple_conditions_all") + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey1) + .withValue(traitValue1), + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey2) + .withValue(traitValue2))))); } - public static SegmentModel segmentMultipleConditionsAny() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); - segmentCondition.setValue(traitValue1); - - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); - segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); - segmentCondition2.setValue(traitValue2); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - - SegmentModel segment = new SegmentModel(); - segment.setId(4); - segment.setName("segment_multiple_conditions_any"); - segment.setRules(Arrays.asList(segmentRule)); - - return segment; + public static SegmentContext segmentMultipleConditionsAny() { + return new SegmentContext().withKey("4").withName("segment_multiple_conditions_any") + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey1) + .withValue(traitValue1), + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey2) + .withValue(traitValue2))))); } - public static SegmentModel segmentNestedRules() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); - segmentCondition.setValue(traitValue1); - - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); - segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); - segmentCondition2.setValue(traitValue2); - - SegmentConditionModel segmentCondition3 = new SegmentConditionModel(); - segmentCondition3.setOperator(SegmentConditions.EQUAL); - segmentCondition3.setProperty_(traitKey3); - segmentCondition3.setValue(traitValue3); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - - SegmentRuleModel segmentRule2 = new SegmentRuleModel(); - segmentRule2.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule2.setConditions(Arrays.asList(segmentCondition3)); - - SegmentRuleModel segmentRule3 = new SegmentRuleModel(); - segmentRule3.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule3.setRules(Arrays.asList(segmentRule, segmentRule2)); - - SegmentModel segment = new SegmentModel(); - segment.setId(5); - segment.setName("segment_nested_rules_all"); - segment.setRules(Arrays.asList(segmentRule3)); - - return segment; + public static SegmentContext segmentNestedRules() { + return new SegmentContext().withKey("5").withName("segment_nested_rules_all") + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ALL).withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey1) + .withValue(traitValue1), + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey2) + .withValue(traitValue2))), + new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey3) + .withValue(traitValue3))))))); } - public static SegmentModel segmentConditionsAndNestedRules() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); - segmentCondition.setValue(traitValue1); - - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); - segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); - segmentCondition2.setValue(traitValue2); - - SegmentConditionModel segmentCondition3 = new SegmentConditionModel(); - segmentCondition3.setOperator(SegmentConditions.EQUAL); - segmentCondition3.setProperty_(traitKey3); - segmentCondition3.setValue(traitValue3); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule.setConditions(Arrays.asList(segmentCondition)); - - SegmentRuleModel segmentRule2 = new SegmentRuleModel(); - segmentRule2.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule2.setConditions(Arrays.asList(segmentCondition2)); - - SegmentRuleModel segmentRule3 = new SegmentRuleModel(); - segmentRule3.setType(SegmentRules.ANY_RULE.getRule()); - segmentRule3.setConditions(Arrays.asList(segmentCondition3)); - - segmentRule.setRules(Arrays.asList(segmentRule2, segmentRule3)); - - SegmentModel segment = new SegmentModel(); - segment.setId(6); - segment.setName("segment_multiple_conditions_all_and_nested_rules"); - segment.setRules(Arrays.asList(segmentRule3)); - - return segment; + public static SegmentContext segmentConditionsAndNestedRules() { + return new SegmentContext().withKey("6") + .withName("segment_multiple_conditions_all_and_nested_rules") + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey1) + .withValue(traitValue1))) + .withRules( + Arrays.asList( + new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey2) + .withValue(traitValue2))), + new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( + Arrays.asList( + new SegmentCondition() + .withOperator(SegmentConditions.EQUAL) + .withProperty(traitKey3) + .withValue(traitValue3))))))); } - public static TraitModel firstIdentityTrait() { TraitModel trait = new TraitModel(); trait.setTraitKey(traitKey1); diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 16e4c53c..5bda8dbf 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -1,13 +1,13 @@ package com.flagsmith.flagengine.unit.segments; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import com.flagsmith.flagengine.segments.SegmentConditionModel; +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.SegmentEvaluator; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.flagengine.segments.SegmentRuleModel; import com.flagsmith.flagengine.segments.constants.SegmentConditions; -import com.flagsmith.flagengine.segments.constants.SegmentRules; +import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.models.TraitModel; import static com.flagsmith.flagengine.unit.segments.IdentitySegmentFixtures.*; @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.stream.Stream; @@ -41,106 +42,59 @@ private static Stream identitiesInSegments() { Arguments.of(segmentNestedRules(), threeIdentityTraits(), Boolean.TRUE), Arguments.of(segmentConditionsAndNestedRules(), emptyIdentityTraits(), Boolean.FALSE), Arguments.of(segmentConditionsAndNestedRules(), oneIdentityTrait(), Boolean.FALSE), - Arguments.of(segmentConditionsAndNestedRules(), threeIdentityTraits(), Boolean.TRUE) - ); + Arguments.of(segmentConditionsAndNestedRules(), threeIdentityTraits(), Boolean.TRUE)); } @ParameterizedTest @MethodSource("identitiesInSegments") - public void testIdentityInSegment(SegmentModel segment, List identityTraits, - Boolean expectedResponse) { - IdentityModel mockIdentity = new IdentityModel(); - mockIdentity.setIdentifier("foo"); - mockIdentity.setIdentityTraits(identityTraits); - mockIdentity.setEnvironmentApiKey("api-key"); + public void testContextInSegment(SegmentContext segment, List identityTraits, + Boolean expectedResponse) { - Boolean actualResult = SegmentEvaluator.evaluateIdentityInSegment(mockIdentity, segment, null); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + new EvaluationContext(), "foo", + identityTraits.stream().collect( + java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); + + Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segment); Assertions.assertTrue(actualResult.equals(expectedResponse)); } private static Stream traitExistenceChecks() { return Stream.of( - Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(), false), - Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(), true), - Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(Arrays.asList( - new TraitModel("foo", "bar"))), true), - Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(Arrays.asList( - new TraitModel("foo", "bar"))), false) - ); + Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(), false), + Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(), true), + Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(Arrays.asList( + new TraitModel("foo", "bar"))), true), + Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(Arrays.asList( + new TraitModel("foo", "bar"))), false)); } @ParameterizedTest @MethodSource("traitExistenceChecks") public void testTraitExistenceConditions(SegmentConditions conditionOperator, String conditionProperty, - List traitModels, Boolean expectedResult) { + List traitModels, Boolean expectedResult) { // Given // An identity to test with which has the traits as defined in the DataProvider - IdentityModel identityModel = new IdentityModel(); - identityModel.setIdentifier("foo"); - identityModel.setIdentityTraits(traitModels); - identityModel.setEnvironmentApiKey("api-key"); - - // And a segment which has the operator and property value as defined in the DataProvider - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(conditionOperator); - segmentCondition.setProperty_(conditionProperty); - segmentCondition.setValue(null); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setConditions(new ArrayList<>(Arrays.asList(segmentCondition))); - segmentRule.setType(SegmentRules.ALL_RULE.getRule()); - - SegmentModel segment = new SegmentModel(); - segment.setName("testSegment"); - segment.setRules(new ArrayList<>(Arrays.asList(segmentRule))); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + new EvaluationContext(), "foo", + traitModels.stream().collect( + java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); + + // And a segment which has the operator and property value as defined in the + // DataProvider + SegmentContext segment = new SegmentContext().withName("testSegment").withRules( + Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList(new SegmentCondition() + .withOperator(conditionOperator) + .withProperty(conditionProperty))))); // When // We evaluate whether the identity is in the segment - Boolean inSegment = SegmentEvaluator.evaluateIdentityInSegment(identityModel, segment, null); + Boolean inSegment = SegmentEvaluator.isContextInSegment(context, segment); // Then // The result is as we expect from the DataProvider definition Assertions.assertEquals(inSegment, expectedResult); } - - private static Stream identitiesInSegmentsPercentageSplit() { - return Stream.of( - Arguments.of(null, "Test", Boolean.TRUE), - Arguments.of(1, "Test", Boolean.FALSE)); - } - - @ParameterizedTest - @MethodSource("identitiesInSegmentsPercentageSplit") - public void testIdentityInSegmentPercentageSplitUsesDjangoId(Integer djangoId, String identifier, - Boolean expectedResult) { - // Given - // An identity with djangoId and identifier as defined in the DataProvider - IdentityModel identityModel = new IdentityModel(); - identityModel.setDjangoId(djangoId); - identityModel.setIdentifier(identifier); - identityModel.setEnvironmentApiKey("key"); - - // And a segment with 50% percentage split - SegmentConditionModel segmentCondition = new SegmentConditionModel(); - segmentCondition.setOperator(SegmentConditions.PERCENTAGE_SPLIT); - segmentCondition.setValue("50"); - - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setConditions(new ArrayList<>(Arrays.asList(segmentCondition))); - segmentRule.setType(SegmentRules.ALL_RULE.getRule()); - - SegmentModel segment = new SegmentModel(); - segment.setId(1); - segment.setName("% split"); - segment.setRules(new ArrayList<>(Arrays.asList(segmentRule))); - - // When - // We evaluate whether the identity is in the segment - Boolean result = SegmentEvaluator.evaluateIdentityInSegment(identityModel, segment, null); - - // Then - // The result is as we expect from the DataProvider definition - Assertions.assertEquals(result, expectedResult); - } } diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index 62fae172..6ea940c0 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -1,6 +1,11 @@ package com.flagsmith.flagengine.unit.segments; -import com.flagsmith.flagengine.segments.SegmentConditionModel; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.IdentityContext; +import com.flagsmith.flagengine.SegmentCondition; +import com.flagsmith.flagengine.SegmentContext; +import com.flagsmith.flagengine.SegmentRule; +import com.flagsmith.flagengine.Traits; import com.flagsmith.flagengine.segments.SegmentEvaluator; import com.flagsmith.flagengine.segments.constants.SegmentConditions; @@ -10,6 +15,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.util.Arrays; import java.util.stream.Stream; public class SegmentModelTest { @@ -86,11 +92,11 @@ private static Stream conditionTestData() { Arguments.of(SegmentConditions.IN, 1, "1,2,3,4", true), Arguments.of(SegmentConditions.IN, 1, "", false), Arguments.of(SegmentConditions.IN, 1, "1", true), - // Flagsmith's engine does not evaluate `IN` condition for floats/doubles and booleans + // Flagsmith's engine does not evaluate `IN` condition for floats/doubles and + // booleans // due to ambiguous serialization across supported platforms. Arguments.of(SegmentConditions.IN, 1.5, "1.5", false), - Arguments.of(SegmentConditions.IN, false, "false", false) - ); + Arguments.of(SegmentConditions.IN, false, "false", false)); } @ParameterizedTest @@ -101,12 +107,23 @@ public void testSegmentConditionMatchesTraitValue( String conditionValue, Boolean expectedResponse) { - SegmentConditionModel conditionModel = new SegmentConditionModel(); - conditionModel.setValue(conditionValue); - conditionModel.setOperator(condition); - conditionModel.setProperty_("foo"); + final EvaluationContext context = new EvaluationContext() + .withIdentity( + new IdentityContext().withTraits( + new Traits().withAdditionalProperty("foo", conditionValue))); - Boolean actualResult = SegmentEvaluator.conditionMatchesTraitValue(conditionModel, traitValue); + SegmentContext segmentContext = new SegmentContext().withKey(conditionValue).withRules( + Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList(new SegmentCondition() + .withOperator(condition).withProperty("foo") + .withValue(conditionValue))))); + + new SegmentCondition() + .withOperator(condition).withProperty("foo") + .withValue(conditionValue); + + Boolean actualResult = SegmentEvaluator.isContextInSegment( + context, segmentContext); assertTrue(actualResult.equals(expectedResponse)); } @@ -119,12 +136,19 @@ public void testSemverMatchesTraitValue( String conditionValue, Boolean expectedResponse) { - SegmentConditionModel conditionModel = new SegmentConditionModel(); - conditionModel.setValue(conditionValue); - conditionModel.setOperator(condition); - conditionModel.setProperty_("foo"); + final EvaluationContext context = new EvaluationContext() + .withIdentity( + new IdentityContext().withTraits( + new Traits().withAdditionalProperty("foo", conditionValue))); + + SegmentContext segmentContext = new SegmentContext().withKey(conditionValue).withRules( + Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList(new SegmentCondition() + .withOperator(condition).withProperty("foo") + .withValue(conditionValue))))); - Boolean actualResult = SegmentEvaluator.conditionMatchesTraitValue(conditionModel, traitValue); + Boolean actualResult = SegmentEvaluator.isContextInSegment( + context, segmentContext); assertTrue(actualResult.equals(expectedResponse)); } diff --git a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java index 74efe40d..bc2531f5 100644 --- a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java +++ b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java @@ -11,21 +11,21 @@ import static org.junit.Assert.assertEquals; public class LocalFileHandlerTest { - @Test - public void testLocalFileHandler() throws FlagsmithClientError, IOException { - // Given - File file = File.createTempFile("temp",".txt"); - try (FileWriter fileWriter = new FileWriter(file, true)) { - fileWriter.write(FlagsmithTestHelper.environmentString()); - fileWriter.flush(); - } + @Test + public void testLocalFileHandler() throws FlagsmithClientError, IOException { + // Given + File file = File.createTempFile("temp", ".txt"); + try (FileWriter fileWriter = new FileWriter(file, true)) { + fileWriter.write(FlagsmithTestHelper.environmentString()); + fileWriter.flush(); + } - // When - LocalFileHandler handler = new LocalFileHandler(file.getAbsolutePath()); + // When + LocalFileHandler handler = new LocalFileHandler(file.getAbsolutePath()); - // Then - assertEquals(FlagsmithTestHelper.environmentModel(), handler.getEnvironment()); + // Then + assertEquals(FlagsmithTestHelper.evaluationContext(), handler.getEvaluationContext()); - file.delete(); - } + file.delete(); + } } From 59583bedeef32191bd5a7fb196264df87352c53a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 18:14:08 +0100 Subject: [PATCH 07/62] test fixes --- .../com/flagsmith/mappers/EngineMappers.java | 7 +++---- .../com/flagsmith/FlagsmithTestHelper.java | 5 ++--- .../unit/segments/SegmentEvaluatorTest.java | 5 +++-- .../unit/segments/SegmentModelTest.java | 21 ++++++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index ce542144..325573db 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -39,11 +39,11 @@ public static EvaluationContext mapContextAndIdentityDataToContext( // Create identity context IdentityContext identityContext = new IdentityContext() .withIdentifier(identifier) - .withKey(context.getEnvironment().getKey() + "_" + identifier); + .withKey(context.getEnvironment().getKey() + "_" + identifier) + .withTraits(new Traits()); // Map traits if provided if (traits != null && !traits.isEmpty()) { - Traits identityTraits = new Traits(); for (Map.Entry entry : traits.entrySet()) { Object traitValue = entry.getValue(); // Handle TraitConfig-like objects (maps with "value" key) @@ -53,9 +53,8 @@ public static EvaluationContext mapContextAndIdentityDataToContext( traitValue = traitMap.get("value"); } } - identityTraits.withAdditionalProperty(entry.getKey(), traitValue); + identityContext.getTraits().setAdditionalProperty(entry.getKey(), traitValue); } - identityContext.withTraits(identityTraits); } // Create new evaluation context with identity diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index f16d451a..350aa4f3 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -260,6 +260,7 @@ public static TraitModel trait(String userIdentifier, String key, String value) public static String environmentString() { return "{\n" + " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + + " \"name\": \"Test Environment\",\n" + " \"project\": {\n" + " \"name\": \"Test project\",\n" + " \"organisation\": {\n" + @@ -342,10 +343,8 @@ public static EvaluationContext evaluationContext() { try { return EngineMappers.mapEnvironmentDocumentToContext(MapperFactory.getMapper().readTree(environmentString())); } catch (JsonProcessingException e) { - // environment model json + throw new RuntimeException("Failed to parse environment JSON", e); } - - return null; } public static List getFlags() { diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 5bda8dbf..64a539ce 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -10,6 +10,7 @@ import com.flagsmith.models.TraitModel; import static com.flagsmith.flagengine.unit.segments.IdentitySegmentFixtures.*; +import com.flagsmith.FlagsmithTestHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -51,7 +52,7 @@ public void testContextInSegment(SegmentContext segment, List identi Boolean expectedResponse) { final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( - new EvaluationContext(), "foo", + FlagsmithTestHelper.evaluationContext(), "foo", identityTraits.stream().collect( java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); @@ -77,7 +78,7 @@ public void testTraitExistenceConditions(SegmentConditions conditionOperator, St // Given // An identity to test with which has the traits as defined in the DataProvider final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( - new EvaluationContext(), "foo", + FlagsmithTestHelper.evaluationContext(), "foo", traitModels.stream().collect( java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index 6ea940c0..5a982758 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -8,6 +8,8 @@ import com.flagsmith.flagengine.Traits; import com.flagsmith.flagengine.segments.SegmentEvaluator; import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.FlagsmithTestHelper; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -16,6 +18,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.util.Arrays; +import java.util.Collections; import java.util.stream.Stream; public class SegmentModelTest { @@ -107,10 +110,9 @@ public void testSegmentConditionMatchesTraitValue( String conditionValue, Boolean expectedResponse) { - final EvaluationContext context = new EvaluationContext() - .withIdentity( - new IdentityContext().withTraits( - new Traits().withAdditionalProperty("foo", conditionValue))); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + FlagsmithTestHelper.evaluationContext(), "foo", + Collections.singletonMap("foo", traitValue)); SegmentContext segmentContext = new SegmentContext().withKey(conditionValue).withRules( Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( @@ -118,10 +120,6 @@ public void testSegmentConditionMatchesTraitValue( .withOperator(condition).withProperty("foo") .withValue(conditionValue))))); - new SegmentCondition() - .withOperator(condition).withProperty("foo") - .withValue(conditionValue); - Boolean actualResult = SegmentEvaluator.isContextInSegment( context, segmentContext); @@ -136,10 +134,9 @@ public void testSemverMatchesTraitValue( String conditionValue, Boolean expectedResponse) { - final EvaluationContext context = new EvaluationContext() - .withIdentity( - new IdentityContext().withTraits( - new Traits().withAdditionalProperty("foo", conditionValue))); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + FlagsmithTestHelper.evaluationContext(), "foo", + Collections.singletonMap("foo", traitValue)); SegmentContext segmentContext = new SegmentContext().withKey(conditionValue).withRules( Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( From c55c3394a42cf7f4e51947a2fe069105753093dd Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 18:49:12 +0100 Subject: [PATCH 08/62] wip + fix null contextValue --- .../com/flagsmith/flagengine/segments/SegmentEvaluator.java | 3 +++ .../flagengine/unit/segments/SegmentEvaluatorTest.java | 2 +- .../flagengine/unit/segments/SegmentModelTest.java | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 2f21c4c8..c7c3c0a5 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -156,6 +156,9 @@ private static Boolean contextMatchesCondition( return false; default: + if (contextValue == null) { + return false; + } return TypeCasting.compare(operator, contextValue, conditionValue); } } diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 64a539ce..00a06499 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -58,7 +58,7 @@ public void testContextInSegment(SegmentContext segment, List identi Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segment); - Assertions.assertTrue(actualResult.equals(expectedResponse)); + Assertions.assertEquals(actualResult, expectedResponse); } private static Stream traitExistenceChecks() { diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index 5a982758..9ad013d3 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -11,7 +11,7 @@ import com.flagsmith.mappers.EngineMappers; import com.flagsmith.FlagsmithTestHelper; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -123,7 +123,7 @@ public void testSegmentConditionMatchesTraitValue( Boolean actualResult = SegmentEvaluator.isContextInSegment( context, segmentContext); - assertTrue(actualResult.equals(expectedResponse)); + assertEquals(expectedResponse, actualResult); } @ParameterizedTest @@ -147,7 +147,7 @@ public void testSemverMatchesTraitValue( Boolean actualResult = SegmentEvaluator.isContextInSegment( context, segmentContext); - assertTrue(actualResult.equals(expectedResponse)); + assertEquals(expectedResponse, actualResult); } private static Stream semverTestData() { From f72f1ff399057a174e4f49164b09044ac5e67994 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 18:49:53 +0100 Subject: [PATCH 09/62] use correct module --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index b617d21b..b87ebfa1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"] path = src/test/java/com/flagsmith/flagengine/enginetestdata - url = git@github.com:Flagsmith/engine-test-data.git - branch = v1.0.0 + url = https://github.com/flagsmith/engine-test-data.git + branch = feat/context-values From e697b61fe739fd0e9885e5b124cd26c76013c9c5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 20:43:38 +0100 Subject: [PATCH 10/62] wip + fix segment tests --- .../flagengine/segments/SegmentEvaluator.java | 50 +++-- .../segments/IdentitySegmentFixtures.java | 180 +++++++++++------- .../unit/segments/SegmentEvaluatorTest.java | 2 +- 3 files changed, 143 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index c7c3c0a5..1eb8ca5e 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -18,17 +18,19 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; +import java.util.stream.Stream; public class SegmentEvaluator { private static ObjectMapper mapper = new ObjectMapper(); private static Configuration jacksonNodeConf = Configuration.builder() - .jsonProvider(new JacksonJsonNodeJsonProvider()) - .mappingProvider(new JacksonMappingProvider(mapper)) - .options(Option.DEFAULT_PATH_LEAF_TO_NULL) - .build(); + .jsonProvider(new JacksonJsonNodeJsonProvider()) + .mappingProvider(new JacksonMappingProvider(mapper)) + .options(Option.DEFAULT_PATH_LEAF_TO_NULL) + .build(); /** * Check if context is in segment. @@ -39,24 +41,38 @@ public class SegmentEvaluator { */ public static Boolean isContextInSegment(EvaluationContext context, SegmentContext segment) { List rules = segment.getRules(); - return rules.stream().allMatch((rule) -> contextMatchesRule(context, rule, segment.getKey())); + return !rules.isEmpty() && rules.stream() + .allMatch((rule) -> contextMatchesRule(context, rule, segment.getKey())); } private static Boolean contextMatchesRule(EvaluationContext context, SegmentRule rule, String segmentKey) { - switch (rule.getType()) { - case ALL: - return rule.getConditions().stream() - .allMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); - case ANY: - return rule.getConditions().stream() - .anyMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); - case NONE: - return rule.getConditions().stream() - .noneMatch((condition) -> contextMatchesCondition(context, condition, segmentKey)); - default: - return false; + 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 isMatch && rule.getRules().stream() + .allMatch((subRule) -> contextMatchesRule(context, subRule, segmentKey)); } private static Boolean contextMatchesCondition( diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java b/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java index 778c64f4..af889785 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/IdentitySegmentFixtures.java @@ -38,88 +38,126 @@ public static SegmentContext segmentSingleCondition() { } public static SegmentContext segmentMultipleConditionsAll() { - return new SegmentContext().withKey("3").withName("segment_multiple_conditions_all") - .withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey1) - .withValue(traitValue1), - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey2) - .withValue(traitValue2))))); + SegmentCondition segmentCondition = new SegmentCondition(); + segmentCondition.setOperator(SegmentConditions.EQUAL); + segmentCondition.setProperty(traitKey1); + segmentCondition.setValue(traitValue1); + + SegmentCondition segmentCondition2 = new SegmentCondition(); + segmentCondition2.setOperator(SegmentConditions.EQUAL); + segmentCondition2.setProperty(traitKey2); + segmentCondition2.setValue(traitValue2); + + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ALL); + segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); + + SegmentContext segment = new SegmentContext(); + segment.setKey("3"); + segment.setName("segment_multiple_conditions_all"); + segment.setRules(Arrays.asList(segmentRule)); + + return segment; } public static SegmentContext segmentMultipleConditionsAny() { - return new SegmentContext().withKey("4").withName("segment_multiple_conditions_any") - .withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey1) - .withValue(traitValue1), - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey2) - .withValue(traitValue2))))); + SegmentCondition segmentCondition = new SegmentCondition(); + segmentCondition.setOperator(SegmentConditions.EQUAL); + segmentCondition.setProperty(traitKey1); + segmentCondition.setValue(traitValue1); + + SegmentCondition segmentCondition2 = new SegmentCondition(); + segmentCondition2.setOperator(SegmentConditions.EQUAL); + segmentCondition2.setProperty(traitKey2); + segmentCondition2.setValue(traitValue2); + + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); + segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); + + SegmentContext segment = new SegmentContext(); + segment.setKey("4"); + segment.setName("segment_multiple_conditions_any"); + segment.setRules(Arrays.asList(segmentRule)); + + return segment; } public static SegmentContext segmentNestedRules() { - return new SegmentContext().withKey("5").withName("segment_nested_rules_all") - .withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ALL).withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey1) - .withValue(traitValue1), - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey2) - .withValue(traitValue2))), - new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey3) - .withValue(traitValue3))))))); + SegmentCondition segmentCondition = new SegmentCondition(); + segmentCondition.setOperator(SegmentConditions.EQUAL); + segmentCondition.setProperty(traitKey1); + segmentCondition.setValue(traitValue1); + + SegmentCondition segmentCondition2 = new SegmentCondition(); + segmentCondition2.setOperator(SegmentConditions.EQUAL); + segmentCondition2.setProperty(traitKey2); + segmentCondition2.setValue(traitValue2); + + SegmentCondition segmentCondition3 = new SegmentCondition(); + segmentCondition3.setOperator(SegmentConditions.EQUAL); + segmentCondition3.setProperty(traitKey3); + segmentCondition3.setValue(traitValue3); + + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); + segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); + + SegmentRule segmentRule2 = new SegmentRule(); + segmentRule2.setType(SegmentRule.Type.ANY); + segmentRule2.setConditions(Arrays.asList(segmentCondition3)); + + SegmentRule segmentRule3 = new SegmentRule(); + segmentRule3.setType(SegmentRule.Type.ANY); + segmentRule3.setRules(Arrays.asList(segmentRule, segmentRule2)); + + SegmentContext segment = new SegmentContext(); + segment.setKey("5"); + segment.setName("segment_nested_rules_all"); + segment.setRules(Arrays.asList(segmentRule3)); + + return segment; } public static SegmentContext segmentConditionsAndNestedRules() { - return new SegmentContext().withKey("6") - .withName("segment_multiple_conditions_all_and_nested_rules") - .withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey1) - .withValue(traitValue1))) - .withRules( - Arrays.asList( - new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey2) - .withValue(traitValue2))), - new SegmentRule().withType(SegmentRule.Type.ANY).withConditions( - Arrays.asList( - new SegmentCondition() - .withOperator(SegmentConditions.EQUAL) - .withProperty(traitKey3) - .withValue(traitValue3))))))); + SegmentCondition segmentCondition = new SegmentCondition(); + segmentCondition.setOperator(SegmentConditions.EQUAL); + segmentCondition.setProperty(traitKey1); + segmentCondition.setValue(traitValue1); + + SegmentCondition segmentCondition2 = new SegmentCondition(); + segmentCondition2.setOperator(SegmentConditions.EQUAL); + segmentCondition2.setProperty(traitKey2); + segmentCondition2.setValue(traitValue2); + + SegmentCondition segmentCondition3 = new SegmentCondition(); + segmentCondition3.setOperator(SegmentConditions.EQUAL); + segmentCondition3.setProperty(traitKey3); + segmentCondition3.setValue(traitValue3); + + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); + segmentRule.setConditions(Arrays.asList(segmentCondition)); + + SegmentRule segmentRule2 = new SegmentRule(); + segmentRule2.setType(SegmentRule.Type.ANY); + segmentRule2.setConditions(Arrays.asList(segmentCondition2)); + + SegmentRule segmentRule3 = new SegmentRule(); + segmentRule3.setType(SegmentRule.Type.ANY); + segmentRule3.setConditions(Arrays.asList(segmentCondition3)); + + segmentRule.setRules(Arrays.asList(segmentRule2, segmentRule3)); + + SegmentContext segment = new SegmentContext(); + segment.setKey("6"); + segment.setName("segment_multiple_conditions_all_and_nested_rules"); + segment.setRules(Arrays.asList(segmentRule3)); + + return segment; } + public static TraitModel firstIdentityTrait() { TraitModel trait = new TraitModel(); trait.setTraitKey(traitKey1); diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 00a06499..f039ba32 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -58,7 +58,7 @@ public void testContextInSegment(SegmentContext segment, List identi Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segment); - Assertions.assertEquals(actualResult, expectedResponse); + Assertions.assertEquals(expectedResponse, actualResult); } private static Stream traitExistenceChecks() { From 4dcdaeda88ab304fb03f1abec8d7b4a6a8969bdd Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 21:01:48 +0100 Subject: [PATCH 11/62] fix assertEquals --- .../java/com/flagsmith/flagengine/SegmentCondition.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java index 33a58dc6..9d7c326c 100644 --- a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java +++ b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java @@ -2,17 +2,12 @@ import com.flagsmith.flagengine.segments.constants.SegmentConditions; import java.util.List; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +@Data public class SegmentCondition { - @Getter - @Setter private SegmentConditions operator; - @Getter private Object value; - @Getter - @Setter private String property; /** From 7be7e3d4094657ffc73da4b784effd3a3fee50a5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 15 Sep 2025 21:47:45 +0100 Subject: [PATCH 12/62] wip + fix `django_id` --- .../com/flagsmith/mappers/EngineMappers.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 325573db..68556a4f 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -276,6 +276,24 @@ private static List mapEnvironmentDocumentFeatureStatesToFeature 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(JsonNode featureState) { + JsonNode node = featureState.get("django_id"); + if (node != null && !node.isNull()) { + return node.asText(); + } + node = featureState.get("featurestate_uuid"); + if (node != null && !node.isNull()) { + return node.asText(); + } + return ""; + } + /** * Maps a single feature state to feature context. * @@ -286,7 +304,7 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt JsonNode feature = featureState.get("feature"); FeatureContext featureContext = new FeatureContext() - .withKey(featureState.get("id").asText()) + .withKey(getFeatureStateKey(featureState)) .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) From c4750b48dace29c947a8703ec6d7ea33b14ba2c7 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 12:35:44 +0100 Subject: [PATCH 13/62] fix engine and evaluator tests --- .../java/com/flagsmith/flagengine/Engine.java | 15 ++-- .../flagengine/segments/SegmentEvaluator.java | 16 ++++- .../flagengine/utils/types/TypeCasting.java | 24 ++++--- .../com/flagsmith/mappers/EngineMappers.java | 35 +++++++--- .../flagsmith/models/FeatureStateModel.java | 68 ------------------- src/main/java/com/flagsmith/models/Flag.java | 5 +- src/main/java/com/flagsmith/models/Flags.java | 23 ++----- .../com/flagsmith/FlagsmithTestHelper.java | 2 +- .../com/flagsmith/flagengine/EngineTest.java | 7 +- .../com/flagsmith/flagengine/enginetestdata | 2 +- .../unit/segments/SegmentModelTest.java | 22 +++--- 11 files changed, 87 insertions(+), 132 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index c5191574..8631f6df 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -21,12 +21,13 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { - SegmentResult segmentResult = new SegmentResult().withKey(segmentContext.getKey()) - .withName(segmentContext.getName()); - segments.add(segmentResult); + segments.add(new SegmentResult().withKey(segmentContext.getKey()) + .withName(segmentContext.getName())); - if (segmentContext.getOverrides() != null) { - for (FeatureContext featureContext : segmentContext.getOverrides()) { + List segmentOverrides = segmentContext.getOverrides(); + + if (segmentOverrides != null) { + for (FeatureContext featureContext : segmentOverrides) { String featureKey = featureContext.getFeatureKey(); if (segmentFeatureContexts.containsKey(featureKey)) { @@ -41,7 +42,7 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { ? Double.POSITIVE_INFINITY : featureContext.getPriority(); - if (existingPriority > featurePriority) { + if (existingPriority < featurePriority) { continue; } } @@ -66,7 +67,7 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { .withFeatureKey(featureContext.getFeatureKey()) .withName(featureContext.getName()) .withValue(featureContext.getValue()) - .withReason("TARGETING MATCH; segment=" + segmentNameWithFeatureContext.getLeft())); + .withReason("TARGETING_MATCH; segment=" + segmentNameWithFeatureContext.getLeft())); } else { flags.add(getFlagResultFromFeatureContext(featureContext, identityKey)); } diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 1eb8ca5e..d9fc2987 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -91,7 +91,7 @@ private static Boolean contextMatchesCondition( List maybeConditionList = (List) conditionValue; conditionList = maybeConditionList.stream() .filter(String.class::isInstance) - .map(String.class::cast) + .map(Object::toString) .collect(Collectors.toList()); } else if (conditionValue instanceof String) { String stringConditionValue = (String) conditionValue; @@ -106,7 +106,11 @@ private static Boolean contextMatchesCondition( } } - return conditionList.contains(contextValue.toString()); + if (!(contextValue instanceof Boolean)) { + contextValue = contextValue.toString(); + } + + return conditionList.contains(contextValue); case PERCENTAGE_SPLIT: String key = (contextValue != null) ? contextValue.toString() @@ -175,7 +179,13 @@ private static Boolean contextMatchesCondition( if (contextValue == null) { return false; } - return TypeCasting.compare(operator, contextValue, conditionValue); + try { + return TypeCasting.compare(operator, contextValue, conditionValue); + } catch (Exception e) { + throw new RuntimeException( + "Error comparing values: " + + String.valueOf(contextValue) + " and " + String.valueOf(conditionValue)); + } } } 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..f7952883 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -11,8 +11,8 @@ public class TypeCasting { * Compare the values value1 and value2 with the provided condition. * * @param condition SegmentCondition criteria to compare values against. - * @param value1 Value to compare. - * @param value2 Value to compare against. + * @param value1 Value to compare. + * @param value2 Value to compare against. */ public static Boolean compare(SegmentConditions condition, Object value1, Object value2) { @@ -39,8 +39,8 @@ public static Boolean compare(SegmentConditions condition, Object value1, Object * Run comparison with condition of primitive type. * * @param condition SegmentCondition criteria to compare values against. - * @param value1 Value to compare. - * @param value2 Value to compare against. + * @param value1 Value to compare. + * @param value2 Value to compare against. */ public static Boolean compare(SegmentConditions condition, Comparable value1, Comparable value2) { if (condition.equals(SegmentConditions.EQUAL)) { @@ -163,7 +163,8 @@ public static Boolean isBoolean(Object str) { public static ComparableVersion toSemver(Object str) { try { String value = SemanticVersioning.isSemver((String) str) - ? SemanticVersioning.removeSemver((String) str) : ((String) str); + ? SemanticVersioning.removeSemver((String) str) + : ((String) str); return new ComparableVersion(value); } catch (Exception nfe) { return null; @@ -180,14 +181,17 @@ public static Boolean isSemver(Object str) { } /** - * Modulo is a special case as the condition value holds both the divisor and remainder. - * This method compares the conditionValue and the traitValue by dividing the traitValue + * Modulo is a special case as the condition value holds both the divisor and + * remainder. + * This method compares the conditionValue and the traitValue by dividing the + * traitValue * by the divisor and verifying that it correctly equals the remainder. * * @param conditionValue conditionValue in the format 'divisor|remainder' - * @param traitValue the value of the matched trait - * @return true if expression evaluates to true, false if unable to evaluate expression or - * it evaluates to false + * @param traitValue the value of the matched trait + * @return true if expression evaluates to true, false if unable to evaluate + * expression or + * it evaluates to false */ public static Boolean compareModulo(String conditionValue, Object traitValue) { try { diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 68556a4f..4f74409f 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -105,8 +105,8 @@ public static EvaluationContext mapEnvironmentDocumentToContext( // Map identity overrides JsonNode identityOverrides = environmentDocument.get("identity_overrides"); if (identityOverrides != null && identityOverrides.isArray()) { - Map identityOverrideSegments = - mapIdentityOverridesToSegments(identityOverrides); + Map identityOverrideSegments = mapIdentityOverridesToSegments( + identityOverrides); segments.putAll(identityOverrideSegments); } @@ -163,9 +163,7 @@ private static Map mapIdentityOverridesToSegments( .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(featureState.get("feature_state_value") != null - ? featureState.get("feature_state_value").asText() - : null) + .withValue(getFeatureStateValue(featureState, "feature_state_value")) .withPriority(Double.NEGATIVE_INFINITY); // Highest possible priority overridesKey.add(featureContext); } @@ -234,7 +232,7 @@ private static List mapEnvironmentDocumentRulesToContextRules( SegmentCondition segmentCondition = new SegmentCondition() .withProperty(condition.get("property_").asText()) .withOperator(SegmentConditions.valueOf(condition.get("operator").asText())) - .withValue(condition.get("value")); + .withValue(condition.get("value").asText()); conditions.add(segmentCondition); } } @@ -294,6 +292,24 @@ private static String getFeatureStateKey(JsonNode featureState) { return ""; } + private static Object getFeatureStateValue(JsonNode featureState, String fieldName) { + JsonNode valueNode = featureState.get(fieldName); + if (valueNode.isTextual() || valueNode.isLong()) { + return valueNode.asText(); + } else if (valueNode.isNumber()) { + if (valueNode.isInt()) { + return valueNode.asInt(); + } else { + return valueNode.asDouble(); + } + } else if (valueNode.isBoolean()) { + return valueNode.asBoolean(); + } else if (valueNode.isArray() || valueNode.isObject()) { + return valueNode; + } + return null; + } + /** * Maps a single feature state to feature context. * @@ -308,9 +324,7 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt .withFeatureKey(feature.get("id").asText()) .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(featureState.get("feature_state_value") != null - ? featureState.get("feature_state_value").asText() - : null); + .withValue(getFeatureStateValue(featureState, "feature_state_value")); // Handle multivariate feature state values JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); @@ -321,7 +335,8 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt sortedMultivariate.sort((a, b) -> a.get("id").asText().compareTo(b.get("id").asText())); for (JsonNode multivariateValue : sortedMultivariate) { FeatureValue variant = new FeatureValue() - .withValue(multivariateValue.get("multivariate_feature_option").get("value").asText()) + .withValue(getFeatureStateValue( + multivariateValue.get("multivariate_feature_option"), "value")) .withWeight(multivariateValue.get("percentage_allocation").asDouble()); variants.add(variant); } diff --git a/src/main/java/com/flagsmith/models/FeatureStateModel.java b/src/main/java/com/flagsmith/models/FeatureStateModel.java index 54c42e7e..6027e9c1 100644 --- a/src/main/java/com/flagsmith/models/FeatureStateModel.java +++ b/src/main/java/com/flagsmith/models/FeatureStateModel.java @@ -1,12 +1,9 @@ package com.flagsmith.models; 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 @@ -63,69 +60,4 @@ public Float getSortValue() { 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; - } - - /** - * 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/models/Flag.java b/src/main/java/com/flagsmith/models/Flag.java index 1c9e3a45..7589526f 100644 --- a/src/main/java/com/flagsmith/models/Flag.java +++ b/src/main/java/com/flagsmith/models/Flag.java @@ -12,13 +12,12 @@ public class Flag extends BaseFlag { * return flag from feature state model and identity id. * * @param featureState feature state model - * @param identityId identity id */ - public static Flag fromFeatureStateModel(FeatureStateModel featureState, Object identityId) { + public static Flag fromFeatureStateModel(FeatureStateModel featureState) { Flag flag = new Flag(); flag.setFeatureId(featureState.getFeature().getId()); - flag.setValue(featureState.getValue(identityId)); + flag.setValue(featureState.getValue()); flag.setFeatureName(featureState.getFeature().getName()); flag.setEnabled(featureState.getEnabled()); diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index d6957f47..467e1a16 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -28,7 +28,7 @@ public class Flags { public static Flags fromFeatureStateModels( List featureStates, AnalyticsProcessor analyticsProcessor) { - return fromFeatureStateModels(featureStates, analyticsProcessor, null, null); + return fromFeatureStateModels(featureStates, analyticsProcessor, null); } /** @@ -36,33 +36,18 @@ public static Flags fromFeatureStateModels( * * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor - * @param identityId identity ID (optional) - */ - public static Flags fromFeatureStateModels( - List featureStates, - AnalyticsProcessor analyticsProcessor, - Object identityId) { - return fromFeatureStateModels(featureStates, analyticsProcessor, identityId, null); - } - - /** - * Build flags object from list of feature states. - * - * @param featureStates list of feature states - * @param analyticsProcessor instance of analytics processor - * @param identityId identity ID (optional) * @param defaultFlagHandler default flags (optional) */ public static Flags fromFeatureStateModels( List featureStates, AnalyticsProcessor analyticsProcessor, - Object identityId, DefaultFlagHandler defaultFlagHandler) { + DefaultFlagHandler defaultFlagHandler) { Map flagMap = featureStates.stream() .collect( Collectors.toMap( (fs) -> fs.getFeature().getName(), - (fs) -> Flag.fromFeatureStateModel(fs, identityId))); + (fs) -> Flag.fromFeatureStateModel(fs))); Flags flags = new Flags(); flags.setFlags(flagMap); @@ -117,7 +102,7 @@ public static Flags fromApiFlags( for (FeatureStateModel flag : apiFlags) { flagMap.put( flag.getFeature().getName(), - Flag.fromFeatureStateModel(flag, null)); + Flag.fromFeatureStateModel(flag)); } Flags flags = new Flags(); diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index 350aa4f3..7399ccfb 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -239,7 +239,7 @@ public static BaseFlag flag( feature.setName(name); feature.setType(type); - return Flag.fromFeatureStateModel(result, null); + return Flag.fromFeatureStateModel(result); } public static BaseFlag flag(String name, String description, boolean enabled) { diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index 0773ed13..9254177b 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -57,6 +57,10 @@ private static Stream engineTestData() { EvaluationContext evaluationContext = EngineMappers.mapContextAndIdentityDataToContext( baseEvaluationContext, identity.get("identifier").asText(), traits); + if (identity.hasNonNull("django_id")) { + evaluationContext.getIdentity().setKey(identity.get("django_id").asText()); + } + JsonNode expectedResponse = identityAndResponse.get("response"); returnValues.add(Arguments.of(evaluationContext, expectedResponse)); @@ -67,7 +71,8 @@ private static Stream engineTestData() { return returnValues.stream(); } catch (Exception e) { - System.out.println(e.getMessage()); + System.out.println("Exception in engineTestData: " + e.getMessage()); + e.printStackTrace(); } return null; } diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index 71a96319..eb256814 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit 71a963198d66d681d12f2bf92c42a3036ffe92a7 +Subproject commit eb256814e08eb5703314abf6485bbd7bf0852c7a diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index 9ad013d3..7a3d7fe8 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -95,10 +95,13 @@ private static Stream conditionTestData() { Arguments.of(SegmentConditions.IN, 1, "1,2,3,4", true), Arguments.of(SegmentConditions.IN, 1, "", false), Arguments.of(SegmentConditions.IN, 1, "1", true), - // Flagsmith's engine does not evaluate `IN` condition for floats/doubles and - // booleans + Arguments.of(SegmentConditions.IN, 1, "[1]", true), + Arguments.of(SegmentConditions.IN, 1, "[\"1\"]", true), + Arguments.of(SegmentConditions.IN, "bar", "[\"bar\"]", true), + Arguments.of(SegmentConditions.IN, "bar", Arrays.asList("bar", "foo"), true), + Arguments.of(SegmentConditions.IN, 1.5, "1.5", true), + // Flagsmith's engine does not evaluate `IN` condition for booleans // due to ambiguous serialization across supported platforms. - Arguments.of(SegmentConditions.IN, 1.5, "1.5", false), Arguments.of(SegmentConditions.IN, false, "false", false)); } @@ -107,18 +110,19 @@ private static Stream conditionTestData() { public void testSegmentConditionMatchesTraitValue( SegmentConditions condition, Object traitValue, - String conditionValue, + Object conditionValue, Boolean expectedResponse) { final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( FlagsmithTestHelper.evaluationContext(), "foo", Collections.singletonMap("foo", traitValue)); - SegmentContext segmentContext = new SegmentContext().withKey(conditionValue).withRules( - Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( - Arrays.asList(new SegmentCondition() - .withOperator(condition).withProperty("foo") - .withValue(conditionValue))))); + SegmentContext segmentContext = new SegmentContext().withKey( + conditionValue.toString()).withRules( + Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( + Arrays.asList(new SegmentCondition() + .withOperator(condition).withProperty("foo") + .withValue(conditionValue))))); Boolean actualResult = SegmentEvaluator.isContextInSegment( context, segmentContext); From 15155e6a2651bac3c61aa196e197e83c58c5d3d7 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 20:19:18 +0100 Subject: [PATCH 14/62] build success --- .../flagengine/segments/SegmentEvaluator.java | 19 +++++++++---------- src/main/java/com/flagsmith/models/Flags.java | 2 +- .../com/flagsmith/FlagsmithClientTest.java | 7 ++++++- .../com/flagsmith/flagengine/EngineTest.java | 6 +++--- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index d9fc2987..2734cc80 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -12,25 +12,21 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; -import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; -import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; -import java.util.stream.Stream; public class SegmentEvaluator { private static ObjectMapper mapper = new ObjectMapper(); - private static Configuration jacksonNodeConf = Configuration.builder() - .jsonProvider(new JacksonJsonNodeJsonProvider()) - .mappingProvider(new JacksonMappingProvider(mapper)) - .options(Option.DEFAULT_PATH_LEAF_TO_NULL) - .build(); + private static Configuration jsonPathConfiguration = Configuration + .defaultConfiguration() + .setOptions(Option.SUPPRESS_EXCEPTIONS); /** * Check if context is in segment. @@ -107,7 +103,7 @@ private static Boolean contextMatchesCondition( } if (!(contextValue instanceof Boolean)) { - contextValue = contextValue.toString(); + contextValue = String.valueOf(contextValue); } return conditionList.contains(contextValue); @@ -198,7 +194,10 @@ private static Boolean contextMatchesCondition( */ private static Object getContextValue(EvaluationContext context, String property) { if (property.startsWith("$.")) { - return JsonPath.using(jacksonNodeConf).parse(mapper.valueToTree(context)).read(property); + return JsonPath + .using(jsonPathConfiguration) + .parse(mapper.convertValue(context, Map.class)) + .read(property); } if (context.getIdentity() != null) { return context.getIdentity().getTraits().getAdditionalProperties().get(property); diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index 467e1a16..9a19324f 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -127,7 +127,7 @@ public static Flags fromEvaluationResult( Map flagMap = evaluationResult.getFlags().stream() .collect( Collectors.toMap( - (fs) -> fs.getFeatureKey(), + (fs) -> fs.getName(), (fs) -> { Flag flag = new Flag(); flag.setFeatureName(fs.getName()); diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 4b68e1e7..07404ea4 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -624,12 +624,17 @@ public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentRetu // Given EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + FlagsmithConfig config = FlagsmithConfig.newBuilder() + .withLocalEvaluation(true) + .build(); + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); when(mockApiWrapper.getEvaluationContext()).thenReturn(evaluationContext); + when(mockApiWrapper.getConfig()).thenReturn(config); FlagsmithClient client = FlagsmithClient.newBuilder() .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .withConfiguration(config) .setApiKey("ser.dummy-key") .build(); diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index 9254177b..b8c93d58 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -97,9 +97,9 @@ public void testEngine(EvaluationContext evaluationContext, JsonNode expectedRes FeatureStateModel fsm = flags.get(i); FlagResult fr = sortedResults.get(i); - assertEquals(fr.getName(), fsm.getFeature().getName()); - assertEquals(fr.getEnabled(), fsm.getEnabled()); - assertEquals(fr.getValue(), fsm.getValue()); + assertEquals(fsm.getFeature().getName(), fr.getName()); + assertEquals(fsm.getEnabled(), fr.getEnabled()); + assertEquals(fsm.getValue(), fr.getValue()); } } } \ No newline at end of file From bf4dddfc8bebe1e7180fd3d7b536fffa2b6a34ea Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 20:32:25 +0100 Subject: [PATCH 15/62] drop Java 8 support --- .github/workflows/run-tests.yml | 2 +- pom.xml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f3af1111..4399b06e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - java: [ "8", "11", "17", "21" ] + java: [ "11", "17", "21" ] distribution: [ "zulu", "adopt" ] steps: diff --git a/pom.xml b/pom.xml index ff9ecfde..0d7efab8 100644 --- a/pom.xml +++ b/pom.xml @@ -38,9 +38,11 @@ + 11 + 11 UTF-8 UTF-8 - 1.11 + 11 2.15.2 1.18.34 1.7.30 @@ -222,8 +224,8 @@ maven-compiler-plugin 3.7.0 - 1.8 - 1.8 + 11 + 11 org.projectlombok From 9d4b0d834cceac0603aee9a6026c46fe670293b9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 20:36:41 +0100 Subject: [PATCH 16/62] maybe use git url --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index b87ebfa1..001db298 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"] path = src/test/java/com/flagsmith/flagengine/enginetestdata - url = https://github.com/flagsmith/engine-test-data.git + url = git@github.com:Flagsmith/engine-test-data.git branch = feat/context-values From afdaec0e90ab855d4347f517fb82d2f46d3ea663 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 21:50:22 +0100 Subject: [PATCH 17/62] bump submodule --- src/test/java/com/flagsmith/flagengine/enginetestdata | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index eb256814..a7847fca 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit eb256814e08eb5703314abf6485bbd7bf0852c7a +Subproject commit a7847fcaa16cf72f68e88215c690e9399fc0d807 From 6a7e1c3412d3d6b81d49f9fdae67811f30acdc69 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 22:22:01 +0100 Subject: [PATCH 18/62] delete more code --- src/main/java/com/flagsmith/FlagsmithClient.java | 4 ---- .../flagsmith/flagengine/models/ResponseJSON.java | 12 ------------ 2 files changed, 16 deletions(-) delete mode 100644 src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 2dd85f22..2c969fc5 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -12,14 +12,10 @@ import com.flagsmith.interfaces.FlagsmithSdk; import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.BaseFlag; -import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; -import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.Segment; -import com.flagsmith.models.TraitModel; import com.flagsmith.threads.PollingManager; import com.flagsmith.utils.ModelUtils; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java b/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java deleted file mode 100644 index 8cf7b89f..00000000 --- a/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.flagsmith.flagengine.models; - -import com.flagsmith.models.FeatureStateModel; -import com.flagsmith.models.TraitModel; -import java.util.List; -import lombok.Data; - -@Data -public class ResponseJSON { - private List flags; - private List traits; -} From c742ec31f4fa862df4ef79675c118bb8cfd1de47 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 22:36:15 +0100 Subject: [PATCH 19/62] minimise formatting changes --- .../flagengine/utils/types/TypeCasting.java | 11 ++++---- .../flagsmith/interfaces/FlagsmithSdk.java | 3 ++- src/main/java/com/flagsmith/models/Flags.java | 10 +++---- .../java/com/flagsmith/utils/ModelUtils.java | 27 +++++++++++-------- 4 files changed, 28 insertions(+), 23 deletions(-) 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 f7952883..45afe5be 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -11,8 +11,8 @@ public class TypeCasting { * Compare the values value1 and value2 with the provided condition. * * @param condition SegmentCondition criteria to compare values against. - * @param value1 Value to compare. - * @param value2 Value to compare against. + * @param value1 Value to compare. + * @param value2 Value to compare against. */ public static Boolean compare(SegmentConditions condition, Object value1, Object value2) { @@ -39,8 +39,8 @@ public static Boolean compare(SegmentConditions condition, Object value1, Object * Run comparison with condition of primitive type. * * @param condition SegmentCondition criteria to compare values against. - * @param value1 Value to compare. - * @param value2 Value to compare against. + * @param value1 Value to compare. + * @param value2 Value to compare against. */ public static Boolean compare(SegmentConditions condition, Comparable value1, Comparable value2) { if (condition.equals(SegmentConditions.EQUAL)) { @@ -163,8 +163,7 @@ public static Boolean isBoolean(Object str) { public static ComparableVersion toSemver(Object str) { try { String value = SemanticVersioning.isSemver((String) str) - ? SemanticVersioning.removeSemver((String) str) - : ((String) str); + ? SemanticVersioning.removeSemver((String) str) : ((String) str); return new ComparableVersion(value); } catch (Exception nfe) { return null; diff --git a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java index 7d6b4619..c8305786 100644 --- a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java +++ b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java @@ -16,7 +16,8 @@ public interface FlagsmithSdk { Flags getFeatureFlags(boolean doThrow); Flags identifyUserWithTraits( - String identifier, List traits, boolean isTransient, boolean doThrow); + String identifier, List traits, boolean isTransient, boolean doThrow + ); FlagsmithConfig getConfig(); diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index 9a19324f..acc6ba22 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -22,7 +22,7 @@ public class Flags { /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor */ public static Flags fromFeatureStateModels( @@ -34,7 +34,7 @@ public static Flags fromFeatureStateModels( /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler default flags (optional) */ @@ -60,7 +60,7 @@ public static Flags fromFeatureStateModels( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -88,7 +88,7 @@ public static Flags fromApiFlags( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -116,7 +116,7 @@ public static Flags fromApiFlags( /** * Build flags object from evaluation result. * - * @param evaluationResult evaluation result + * @param evaluationResult evaluation result * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ diff --git a/src/main/java/com/flagsmith/utils/ModelUtils.java b/src/main/java/com/flagsmith/utils/ModelUtils.java index d5faa697..253a2538 100644 --- a/src/main/java/com/flagsmith/utils/ModelUtils.java +++ b/src/main/java/com/flagsmith/utils/ModelUtils.java @@ -22,7 +22,7 @@ public class ModelUtils { */ public static List getTraitModelsFromTraitMap(Map traits) { return ModelUtils.getTraitModelStreamFromTraitMap( - traits, () -> new TraitModel()).map(Pair::getLeft).collect(Collectors.toList()); + traits, () -> new TraitModel()).map(Pair::getLeft).collect(Collectors.toList()); } /** @@ -34,23 +34,28 @@ public static List getTraitModelsFromTraitMap(Map tr */ public static List getSdkTraitModelsFromTraitMap(Map traits) { return ModelUtils.getTraitModelStreamFromTraitMap(traits, () -> new SdkTraitModel()).map( - (row) -> { - SdkTraitModel sdkTraitModel = row.getLeft(); - TraitConfig traitConfig = row.getRight(); - sdkTraitModel.setIsTransient(traitConfig.getIsTransient()); - return sdkTraitModel; - }).collect(Collectors.toList()); + (row) -> { + SdkTraitModel sdkTraitModel = row.getLeft(); + TraitConfig traitConfig = row.getRight(); + sdkTraitModel.setIsTransient(traitConfig.getIsTransient()); + return sdkTraitModel; + } + ).collect(Collectors.toList()); } private static Stream> getTraitConfigStreamFromTraitMap( - Map traits) { + Map traits + ) { return traits.entrySet().stream().map( row -> new AbstractMap.SimpleEntry<>( - row.getKey(), TraitConfig.fromObject(row.getValue()))); + row.getKey(), TraitConfig.fromObject(row.getValue())) + ); } - private static Stream> - getTraitModelStreamFromTraitMap(Map traits, Supplier traitSupplier) { + private static Stream> + getTraitModelStreamFromTraitMap( + Map traits, Supplier traitSupplier + ) { return ModelUtils.getTraitConfigStreamFromTraitMap(traits).map( (row) -> { T trait = traitSupplier.get(); From 89f420abd88621ec92b1cffbf9ec1b1fda95bf88 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 22:42:29 +0100 Subject: [PATCH 20/62] minimise formatting changes pt. 2 --- .../com/flagsmith/FlagsmithApiWrapper.java | 62 ++++++++++--------- .../flagengine/utils/types/TypeCasting.java | 13 ++-- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index e605df92..6e1815f0 100644 --- a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java +++ b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java @@ -46,18 +46,19 @@ public class FlagsmithApiWrapper implements FlagsmithSdk { /** * Instantiate with cache. * - * @param cache cache object + * @param cache cache object * @param defaultConfig config object * @param customHeaders custom headers list - * @param logger logger object - * @param apiKey api key + * @param logger logger object + * @param apiKey api key */ public FlagsmithApiWrapper( final FlagsmithCache cache, final FlagsmithConfig defaultConfig, final HashMap customHeaders, final FlagsmithLogger logger, - final String apiKey) { + final String apiKey + ) { this(defaultConfig, customHeaders, logger, apiKey); this.cache = cache; } @@ -67,14 +68,15 @@ public FlagsmithApiWrapper( * * @param defaultConfig config object * @param customHeaders custom headers list - * @param logger logger instance - * @param apiKey api key + * @param logger logger instance + * @param apiKey api key */ public FlagsmithApiWrapper( final FlagsmithConfig defaultConfig, final HashMap customHeaders, final FlagsmithLogger logger, - final String apiKey) { + final String apiKey + ) { this.defaultConfig = defaultConfig; this.customHeaders = customHeaders; this.logger = logger; @@ -82,25 +84,27 @@ public FlagsmithApiWrapper( requestor = new RequestProcessor( defaultConfig.getHttpClient(), logger, - defaultConfig.getRetries()); + defaultConfig.getRetries() + ); } /** * Instantiate with config, custom headers, logger, apikey and request * processor. * - * @param defaultConfig config object - * @param customHeaders custom headers list - * @param logger logger instance - * @param apiKey api key + * @param defaultConfig config object + * @param customHeaders custom headers list + * @param logger logger instance + * @param apiKey api key * @param requestProcessor request processor */ public FlagsmithApiWrapper( - final FlagsmithConfig defaultConfig, - final HashMap customHeaders, - final FlagsmithLogger logger, - final String apiKey, - final RequestProcessor requestProcessor) { + final FlagsmithConfig defaultConfig, + final HashMap customHeaders, + final FlagsmithLogger logger, + final String apiKey, + final RequestProcessor requestProcessor + ) { this.defaultConfig = defaultConfig; this.customHeaders = customHeaders; this.logger = logger; @@ -129,13 +133,14 @@ public Flags getFeatureFlags(boolean doThrow) { Future> featureFlagsFuture = requestor.executeAsync( request, - new TypeReference>() { - }, - doThrow); + new TypeReference>() {}, + doThrow + ); try { List featureFlagsResponse = featureFlagsFuture.get( - TIMEOUT, TimeUnit.MILLISECONDS); + TIMEOUT, TimeUnit.MILLISECONDS + ); if (featureFlagsResponse == null) { featureFlagsResponse = new ArrayList<>(); @@ -202,17 +207,17 @@ public Flags identifyUserWithTraits( Future featureFlagsFuture = requestor.executeAsync( request, - new TypeReference() { - }, - doThrow); + new TypeReference() {}, + doThrow + ); try { FlagsAndTraitsResponse flagsAndTraitsResponse = featureFlagsFuture.get( - TIMEOUT, TimeUnit.MILLISECONDS); + TIMEOUT, TimeUnit.MILLISECONDS + ); List flagsArray = flagsAndTraitsResponse != null && flagsAndTraitsResponse.getFlags() != null - ? flagsAndTraitsResponse.getFlags() - : new ArrayList<>(); + ? flagsAndTraitsResponse.getFlags() : new ArrayList<>(); flags = Flags.fromApiFlags( flagsArray, @@ -246,8 +251,7 @@ public EvaluationContext getEvaluationContext() { final Request request = newGetRequest(defaultConfig.getEnvironmentUri()); Future environmentFuture = requestor.executeAsync(request, - new TypeReference() { - }, + new TypeReference() {}, Boolean.TRUE); try { 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 45afe5be..3ea6d62e 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -180,17 +180,14 @@ public static Boolean isSemver(Object str) { } /** - * Modulo is a special case as the condition value holds both the divisor and - * remainder. - * This method compares the conditionValue and the traitValue by dividing the - * traitValue + * Modulo is a special case as the condition value holds both the divisor and remainder. + * This method compares the conditionValue and the traitValue by dividing the traitValue * by the divisor and verifying that it correctly equals the remainder. * * @param conditionValue conditionValue in the format 'divisor|remainder' - * @param traitValue the value of the matched trait - * @return true if expression evaluates to true, false if unable to evaluate - * expression or - * it evaluates to false + * @param traitValue the value of the matched trait + * @return true if expression evaluates to true, false if unable to evaluate expression or + * it evaluates to false */ public static Boolean compareModulo(String conditionValue, Object traitValue) { try { From d4e5b1ee2ce57334271a55bf68d75941bc383a0e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 23:01:50 +0100 Subject: [PATCH 21/62] minimise formatting changes pt. 3 --- .../com/flagsmith/FlagsmithApiWrapper.java | 17 +++++----- .../flagsmith/FlagsmithApiWrapperTest.java | 18 +++++----- .../com/flagsmith/FlagsmithTestHelper.java | 33 ++++++++++++------- .../com/flagsmith/flagengine/EngineTest.java | 12 +++---- .../unit/segments/SegmentEvaluatorTest.java | 19 ++++++----- .../unit/segments/SegmentModelTest.java | 3 +- .../offline/LocalFileHandlerTest.java | 28 ++++++++-------- 7 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index 6e1815f0..0a05945d 100644 --- a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java +++ b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java @@ -89,8 +89,7 @@ public FlagsmithApiWrapper( } /** - * Instantiate with config, custom headers, logger, apikey and request - * processor. + * Instantiate with config, custom headers, logger, apikey and request processor. * * @param defaultConfig config object * @param customHeaders custom headers list @@ -149,7 +148,8 @@ public Flags getFeatureFlags(boolean doThrow) { featureFlags = Flags.fromApiFlags( featureFlagsResponse, getConfig().getAnalyticsProcessor(), - getConfig().getFlagsmithFlagDefaults()); + getConfig().getFlagsmithFlagDefaults() + ); if (getCache() != null && getCache().getEnvFlagsCacheKey() != null) { getCache().getCache().put(getCache().getEnvFlagsCacheKey(), featureFlags); @@ -173,7 +173,8 @@ public Flags getFeatureFlags(boolean doThrow) { @Override public Flags identifyUserWithTraits( - String identifier, List traits, boolean isTransient, boolean doThrow) { + String identifier, List traits, boolean isTransient, boolean doThrow + ) { assertValidUser(identifier); Flags flags = null; String cacheKey = null; @@ -222,7 +223,8 @@ public Flags identifyUserWithTraits( flags = Flags.fromApiFlags( flagsArray, getConfig().getAnalyticsProcessor(), - getConfig().getFlagsmithFlagDefaults()); + getConfig().getFlagsmithFlagDefaults() + ); if (cacheKey != null) { getCache().getCache().put(cacheKey, flags); @@ -322,7 +324,7 @@ public Request newGetRequest(HttpUrl url) { /** * Returns a build request with GET. * - * @param url - URL to invoke + * @param url - URL to invoke * @param body - body to post */ @Override @@ -334,8 +336,7 @@ public Request newPostRequest(HttpUrl url, RequestBody body) { } /** - * Close the FlagsmithAPIWrapper instance, cleaning up any dependent threads or - * services + * Close the FlagsmithAPIWrapper instance, cleaning up any dependent threads or services * which need cleaning up before the instance can be fully destroyed. */ public void close() { diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java index 2d2c27d2..2f9ef58a 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + public class FlagsmithApiWrapperTest { private final String API_KEY = "OUR_API_KEY"; @@ -80,8 +81,7 @@ void getFeatureFlags_retries() { } // Assert - // Since the Retry object is local to the call, the only external behaviour we - // can watch + // Since the Retry object is local to the call, the only external behaviour we can watch // is the logger verify(flagsmithLogger, times(2)).httpError(any(), any(Response.class), anyBoolean()); } @@ -124,21 +124,22 @@ public void getFeatureFlags_noUser_fail() { public void identifyUserWithTraits_success() throws JsonProcessingException { // Arrange final List traits = new ArrayList(Arrays.asList(new TraitModel())); - String responseBody = mapper - .writeValueAsString(getFlagsAndTraitsResponse(Arrays.asList(getNewFlag()), Arrays.asList(new TraitModel()))); + String responseBody = mapper.writeValueAsString(getFlagsAndTraitsResponse(Arrays.asList(getNewFlag()), Arrays.asList(new TraitModel()))); interceptor.addRule() .post(BASE_URL + "/identities/") .respond(responseBody, MEDIATYPE_JSON); // Act final Flags actualFeatureFlags = sut.identifyUserWithTraits( - "user-w-traits", traits, false, true); + "user-w-traits", traits, false, true + ); // Assert Map flag1 = newFlagsList(Arrays.asList(getNewFlag())).getFlags(); Map flag2 = actualFeatureFlags.getFlags(); assertEquals( - flag1, flag2); + flag1, flag2 + ); verify(flagsmithLogger, times(1)).info(anyString(), any(), any()); verify(flagsmithLogger, times(0)).httpError(any(), any(Response.class), anyBoolean()); verify(flagsmithLogger, times(0)).httpError(any(), any(IOException.class), anyBoolean()); @@ -167,7 +168,7 @@ public void testClose_ClosesRequestProcessor() { // Given RequestProcessor mockedRequestProcessor = mock(RequestProcessor.class); FlagsmithApiWrapper apiWrapper = new FlagsmithApiWrapper( - defaultConfig, null, flagsmithLogger, API_KEY, mockedRequestProcessor); + defaultConfig, null, flagsmithLogger, API_KEY, mockedRequestProcessor); // When apiWrapper.close(); @@ -203,7 +204,8 @@ private FeatureStateModel getNewFlag() { private Flags newFlagsList(List flags) { return Flags.fromApiFlags( - flags, null, defaultConfig.getFlagsmithFlagDefaults()); + flags, null, defaultConfig.getFlagsmithFlagDefaults() + ); } private JsonNode getFlagsAndTraitsResponse(List flags, List traits) { diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index 7399ccfb..5438f06f 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -83,7 +83,8 @@ public static int createUserIdentity(String userIdentity, String environmentApiK return RestAssured.given() .body(ImmutableMap.of( "identifier", userIdentity, - "environment", environmentApiKey)) + "environment", environmentApiKey + )) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/identities/", environmentApiKey) .then() @@ -97,7 +98,8 @@ public static Map createEnvironment(String name, int projectId) return RestAssured.given() .body(ImmutableMap.of( "name", name, - "project", projectId)) + "project", projectId + )) .headers(defaultHeaders()) .post("/api/v1/environments/") .then() @@ -121,7 +123,8 @@ public static void switchFlag(int featureId, boolean enabled, String apiKey) { RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId)) + "feature", featureId + )) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/featurestates/", apiKey) .then() @@ -137,7 +140,8 @@ public static void switchFlag(int featureId, boolean enabled, String apiKey) { RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId)) + "feature", featureId + )) .headers(defaultHeaders()) .put("/api/v1/environments/{apiKey}/featurestates/{featureStateId}/", apiKey, featureStateId) @@ -166,7 +170,8 @@ public static void switchFlagForUser(int featureId, int userIdentityId, boolean RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId)) + "feature", featureId + )) .headers(defaultHeaders()) .post("/api/v1/environments/{apiKey}/identities/{identityId}/featurestates/", apiKey, userIdentityId) @@ -187,7 +192,8 @@ public static void switchFlagForUser(int featureId, int userIdentityId, boolean RestAssured.given() .body(ImmutableMap.of( "enabled", enabled, - "feature", featureId)) + "feature", featureId + )) .headers(defaultHeaders()) .put( "/api/v1/environments/{apiKey}/identities/{identityId}/featurestates/{featureStateId}/", @@ -207,7 +213,8 @@ public static void assignTraitToUserIdentity(String userIdentifier, String trait .body(ImmutableMap.of( "identity", ImmutableMap.of("identifier", userIdentifier), "trait_key", traitKey, - "trait_value", traitValue)) + "trait_value", traitValue + )) .headers(defaultHeaders()) .header("x-environment-key", apiKey) .post("/api/v1/traits/") @@ -219,7 +226,8 @@ public static int createProject(String name, int organisationId) { return RestAssured.given() .body(ImmutableMap.of( "name", name, - "organisation", organisationId)) + "organisation", organisationId + )) .headers(defaultHeaders()) .post("/api/v1/projects/") .then() @@ -230,7 +238,8 @@ public static int createProject(String name, int organisationId) { } public static BaseFlag flag( - String name, String description, String type, boolean enabled, String value) { + String name, String description, String type, boolean enabled, String value + ) { final FeatureStateModel result = new FeatureStateModel(); result.setEnabled(enabled); result.setValue(value); @@ -372,8 +381,8 @@ public static List getFlags() { try { return MapperFactory.getMapper().readValue( featureJson, - new TypeReference>() { - }); + new TypeReference>() {} + ); } catch (JsonProcessingException e) { e.printStackTrace(); // environment model json @@ -430,7 +439,7 @@ public static JsonNode getIdentityRequest(String identifier, List traits, boolean isTransient) { + String identifier, List traits, boolean isTransient) { final ObjectNode flagsAndTraits = MapperFactory.getMapper().createObjectNode(); flagsAndTraits.putPOJO("identifier", identifier); flagsAndTraits.put("transient", isTransient); diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index b8c93d58..4d0162af 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -25,9 +25,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class EngineTest { - private static final String ENVIRONMENT_JSON_FILE_LOCATION = "src/test/java/com/flagsmith/flagengine/enginetestdata/" - + - "data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"; + private static final String ENVIRONMENT_JSON_FILE_LOCATION = + "src/test/java/com/flagsmith/flagengine/enginetestdata/" + + "data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"; private static ObjectMapper objectMapper = MapperFactory.getMapper(); private static Stream engineTestData() { @@ -37,8 +37,7 @@ private static Stream engineTestData() { JsonNode environmentDocument = engineTestData.get("environment"); JsonNode identitiesAndResponses = engineTestData.get("identities_and_responses"); - EvaluationContext baseEvaluationContext = EngineMappers - .mapEnvironmentDocumentToContext(environmentDocument); + EvaluationContext baseEvaluationContext = EngineMappers.mapEnvironmentDocumentToContext(environmentDocument); List returnValues = new ArrayList<>(); @@ -84,8 +83,7 @@ public void testEngine(EvaluationContext evaluationContext, JsonNode expectedRes List flags = objectMapper.convertValue( expectedResponse.get("flags"), - new TypeReference>() { - }); + new TypeReference>() {}); flags.sort((fsm1, fsm2) -> fsm1.getFeature().getName().compareTo(fsm2.getFeature().getName())); List sortedResults = evaluationResult.getFlags().stream() diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index f039ba32..89e0c4eb 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -43,18 +43,20 @@ private static Stream identitiesInSegments() { Arguments.of(segmentNestedRules(), threeIdentityTraits(), Boolean.TRUE), Arguments.of(segmentConditionsAndNestedRules(), emptyIdentityTraits(), Boolean.FALSE), Arguments.of(segmentConditionsAndNestedRules(), oneIdentityTrait(), Boolean.FALSE), - Arguments.of(segmentConditionsAndNestedRules(), threeIdentityTraits(), Boolean.TRUE)); + Arguments.of(segmentConditionsAndNestedRules(), threeIdentityTraits(), Boolean.TRUE) + ); } @ParameterizedTest @MethodSource("identitiesInSegments") public void testContextInSegment(SegmentContext segment, List identityTraits, - Boolean expectedResponse) { + Boolean expectedResponse) { final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( FlagsmithTestHelper.evaluationContext(), "foo", identityTraits.stream().collect( - java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); + java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue)) + ); Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segment); @@ -68,22 +70,23 @@ private static Stream traitExistenceChecks() { Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(Arrays.asList( new TraitModel("foo", "bar"))), true), Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(Arrays.asList( - new TraitModel("foo", "bar"))), false)); + new TraitModel("foo", "bar"))), false) + ); } @ParameterizedTest @MethodSource("traitExistenceChecks") public void testTraitExistenceConditions(SegmentConditions conditionOperator, String conditionProperty, - List traitModels, Boolean expectedResult) { + List traitModels, Boolean expectedResult) { // Given // An identity to test with which has the traits as defined in the DataProvider final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( FlagsmithTestHelper.evaluationContext(), "foo", traitModels.stream().collect( - java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue))); + java.util.stream.Collectors.toMap(TraitModel::getTraitKey, TraitModel::getTraitValue)) + ); - // And a segment which has the operator and property value as defined in the - // DataProvider + // And a segment which has the operator and property value as defined in the DataProvider SegmentContext segment = new SegmentContext().withName("testSegment").withRules( Arrays.asList(new SegmentRule().withType(SegmentRule.Type.ALL).withConditions( Arrays.asList(new SegmentCondition() diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index 7a3d7fe8..d2fc7e4b 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -102,7 +102,8 @@ private static Stream conditionTestData() { Arguments.of(SegmentConditions.IN, 1.5, "1.5", true), // Flagsmith's engine does not evaluate `IN` condition for booleans // due to ambiguous serialization across supported platforms. - Arguments.of(SegmentConditions.IN, false, "false", false)); + Arguments.of(SegmentConditions.IN, false, "false", false) + ); } @ParameterizedTest diff --git a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java index bc2531f5..fdf68183 100644 --- a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java +++ b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java @@ -11,21 +11,21 @@ import static org.junit.Assert.assertEquals; public class LocalFileHandlerTest { - @Test - public void testLocalFileHandler() throws FlagsmithClientError, IOException { - // Given - File file = File.createTempFile("temp", ".txt"); - try (FileWriter fileWriter = new FileWriter(file, true)) { - fileWriter.write(FlagsmithTestHelper.environmentString()); - fileWriter.flush(); - } + @Test + public void testLocalFileHandler() throws FlagsmithClientError, IOException { + // Given + File file = File.createTempFile("temp",".txt"); + try (FileWriter fileWriter = new FileWriter(file, true)) { + fileWriter.write(FlagsmithTestHelper.environmentString()); + fileWriter.flush(); + } - // When - LocalFileHandler handler = new LocalFileHandler(file.getAbsolutePath()); + // When + LocalFileHandler handler = new LocalFileHandler(file.getAbsolutePath()); - // Then - assertEquals(FlagsmithTestHelper.evaluationContext(), handler.getEvaluationContext()); + // Then + assertEquals(FlagsmithTestHelper.evaluationContext(), handler.getEvaluationContext()); - file.delete(); - } + file.delete(); + } } From d99e9d18b49bc702a5ae3affae6dcaccd39e6385 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 23:07:51 +0100 Subject: [PATCH 22/62] improve formatting, comment --- src/main/java/com/flagsmith/flagengine/Engine.java | 6 +++--- .../flagengine/unit/segments/SegmentModelTest.java | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index 8631f6df..8245e247 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -9,10 +9,10 @@ public class Engine { /** - * Evaluate if identity is in segment. + * Get evaluation result for a given evaluation context. * - * @param context Identity Instance. - * @return True adsa. + * @param context Evaluation context. + * @return Evaluation result. */ public static EvaluationResult getEvaluationResult(EvaluationContext context) { List segments = new ArrayList<>(); diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java index d2fc7e4b..ce0da7c6 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -125,8 +125,7 @@ public void testSegmentConditionMatchesTraitValue( .withOperator(condition).withProperty("foo") .withValue(conditionValue))))); - Boolean actualResult = SegmentEvaluator.isContextInSegment( - context, segmentContext); + Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segmentContext); assertEquals(expectedResponse, actualResult); } @@ -149,8 +148,7 @@ public void testSemverMatchesTraitValue( .withOperator(condition).withProperty("foo") .withValue(conditionValue))))); - Boolean actualResult = SegmentEvaluator.isContextInSegment( - context, segmentContext); + Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segmentContext); assertEquals(expectedResponse, actualResult); } From b861a7264fb88be3c529d1cc14e9efd17e32bda6 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 16 Sep 2025 23:17:43 +0100 Subject: [PATCH 23/62] minimise formatting changes pt. 4 --- .../com/flagsmith/FlagsmithTestHelper.java | 158 +++++++++--------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index 5438f06f..7a904f43 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -193,7 +193,7 @@ public static void switchFlagForUser(int featureId, int userIdentityId, boolean .body(ImmutableMap.of( "enabled", enabled, "feature", featureId - )) + )) .headers(defaultHeaders()) .put( "/api/v1/environments/{apiKey}/identities/{identityId}/featurestates/{featureStateId}/", @@ -268,84 +268,84 @@ public static TraitModel trait(String userIdentifier, String key, String value) public static String environmentString() { return "{\n" + - " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + - " \"name\": \"Test Environment\",\n" + - " \"project\": {\n" + - " \"name\": \"Test project\",\n" + - " \"organisation\": {\n" + - " \"feature_analytics\": false,\n" + - " \"name\": \"Test Org\",\n" + - " \"id\": 1,\n" + - " \"persist_trait_data\": true,\n" + - " \"stop_serving_flags\": false\n" + - " },\n" + - " \"id\": 1,\n" + - " \"hide_disabled_flags\": false,\n" + - " \"segments\": [\n" + - " {\n" + - " \"id\": 1,\n" + - " \"name\": \"Test segment\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [\n" + - " {\n" + - " \"type\": \"ALL\",\n" + - " \"rules\": [],\n" + - " \"conditions\": [\n" + - " {\n" + - " \"operator\": \"EQUAL\",\n" + - " \"property_\": \"foo\",\n" + - " \"value\": \"bar\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"segment_overrides\": [],\n" + - " \"id\": 1,\n" + - " \"feature_states\": [\n" + - " {\n" + - " \"multivariate_feature_state_values\": [],\n" + - " \"feature_state_value\": \"some-value\",\n" + - " \"id\": 1,\n" + - " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + - " \"feature\": {\n" + - " \"name\": \"some_feature\",\n" + - " \"type\": \"STANDARD\",\n" + - " \"id\": 1\n" + - " },\n" + - " \"segment_id\": null,\n" + - " \"enabled\": true\n" + - " }\n" + - " ],\n" + - " \"identity_overrides\": [\n" + - " {\n" + - " \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" + - " \"identifier\": \"overridden-identity\",\n" + - " \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" + - " \"identity_features\": [\n" + - " {\n" + - " \"feature_state_value\": \"overridden-value\",\n" + - " \"multivariate_feature_state_values\": [],\n" + - " \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" + - " \"feature\": {\n" + - " \"name\": \"some_feature\",\n" + - " \"type\": \"STANDARD\",\n" + - " \"id\": 1\n" + - " },\n" + - " \"enabled\": true\n" + - " }\n" + - " ],\n" + - " \"identity_traits\": [],\n" + - " \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" + - " }\n" + - " ]\n" + - "}"; + " \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" + + " \"name\": \"Test Environment\",\n" + + " \"project\": {\n" + + " \"name\": \"Test project\",\n" + + " \"organisation\": {\n" + + " \"feature_analytics\": false,\n" + + " \"name\": \"Test Org\",\n" + + " \"id\": 1,\n" + + " \"persist_trait_data\": true,\n" + + " \"stop_serving_flags\": false\n" + + " },\n" + + " \"id\": 1,\n" + + " \"hide_disabled_flags\": false,\n" + + " \"segments\": [\n" + + " {\n" + + " \"id\": 1,\n" + + " \"name\": \"Test segment\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [\n" + + " {\n" + + " \"type\": \"ALL\",\n" + + " \"rules\": [],\n" + + " \"conditions\": [\n" + + " {\n" + + " \"operator\": \"EQUAL\",\n" + + " \"property_\": \"foo\",\n" + + " \"value\": \"bar\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"segment_overrides\": [],\n" + + " \"id\": 1,\n" + + " \"feature_states\": [\n" + + " {\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"feature_state_value\": \"some-value\",\n" + + " \"id\": 1,\n" + + " \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"segment_id\": null,\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_overrides\": [\n" + + " {\n" + + " \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" + + " \"identifier\": \"overridden-identity\",\n" + + " \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" + + " \"identity_features\": [\n" + + " {\n" + + " \"feature_state_value\": \"overridden-value\",\n" + + " \"multivariate_feature_state_values\": [],\n" + + " \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" + + " \"feature\": {\n" + + " \"name\": \"some_feature\",\n" + + " \"type\": \"STANDARD\",\n" + + " \"id\": 1\n" + + " },\n" + + " \"enabled\": true\n" + + " }\n" + + " ],\n" + + " \"identity_traits\": [],\n" + + " \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" + + " }\n" + + " ]\n" + + "}"; } public static EvaluationContext evaluationContext() { From 4730f9a5bec7707d5d2fff1289b776d93b41334f Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 17 Sep 2025 00:03:46 +0100 Subject: [PATCH 24/62] remove debugging code --- .../flagsmith/flagengine/segments/SegmentEvaluator.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 2734cc80..4a59f788 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -175,13 +175,7 @@ private static Boolean contextMatchesCondition( if (contextValue == null) { return false; } - try { - return TypeCasting.compare(operator, contextValue, conditionValue); - } catch (Exception e) { - throw new RuntimeException( - "Error comparing values: " - + String.valueOf(contextValue) + " and " + String.valueOf(conditionValue)); - } + return TypeCasting.compare(operator, contextValue, conditionValue); } } From bebac37e68b65f797444d0414d707d1829a8263c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 17:35:07 +0100 Subject: [PATCH 25/62] use latest schemas and engine-test-data - switch to latest engine-test-data (with the segment conditions null values fix) - switch to latest EvaluationResult schema - fix a case with null context value being coerced to string for the IN operator - fix weight display in SPLIT reasons --- .gitmodules | 2 +- .../java/com/flagsmith/flagengine/Engine.java | 37 ++--- .../flagengine/SegmentCondition.java | 31 +++++ .../flagengine/segments/SegmentEvaluator.java | 2 +- src/main/java/com/flagsmith/models/Flags.java | 27 ++-- .../com/flagsmith/flagengine/EngineTest.java | 127 ++++++------------ .../com/flagsmith/flagengine/enginetestdata | 2 +- 7 files changed, 113 insertions(+), 115 deletions(-) diff --git a/.gitmodules b/.gitmodules index 001db298..b3c2502a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"] path = src/test/java/com/flagsmith/flagengine/enginetestdata url = git@github.com:Flagsmith/engine-test-data.git - branch = feat/context-values + branch = fix/schema-errors diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index 8245e247..3780038a 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -17,7 +17,7 @@ public class Engine { public static EvaluationResult getEvaluationResult(EvaluationContext context) { List segments = new ArrayList<>(); HashMap> segmentFeatureContexts = new HashMap<>(); - List flags = new ArrayList<>(); + Flags flags = new Flags(); for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { @@ -58,22 +58,29 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { ? context.getIdentity().getKey() : null; - for (FeatureContext featureContext : context.getFeatures().getAdditionalProperties().values()) { - if (segmentFeatureContexts.containsKey(featureContext.getFeatureKey())) { - ImmutablePair segmentNameWithFeatureContext = segmentFeatureContexts - .get(featureContext.getFeatureKey()); - featureContext = segmentNameWithFeatureContext.getRight(); - flags.add(new FlagResult().withEnabled(featureContext.getEnabled()) - .withFeatureKey(featureContext.getFeatureKey()) - .withName(featureContext.getName()) - .withValue(featureContext.getValue()) - .withReason("TARGETING_MATCH; segment=" + segmentNameWithFeatureContext.getLeft())); - } else { - flags.add(getFlagResultFromFeatureContext(featureContext, identityKey)); + Features contextFeatures = context.getFeatures(); + if (contextFeatures != null) { + for (FeatureContext featureContext : contextFeatures.getAdditionalProperties().values()) { + if (segmentFeatureContexts.containsKey(featureContext.getFeatureKey())) { + ImmutablePair segmentNameFeaturePair = segmentFeatureContexts + .get(featureContext.getFeatureKey()); + featureContext = segmentNameFeaturePair.getRight(); + flags.setAdditionalProperty( + featureContext.getName(), + new FlagResult().withEnabled(featureContext.getEnabled()) + .withFeatureKey(featureContext.getFeatureKey()) + .withName(featureContext.getName()) + .withValue(featureContext.getValue()) + .withReason( + "TARGETING_MATCH; segment=" + segmentNameFeaturePair.getLeft())); + } else { + flags.setAdditionalProperty(featureContext.getName(), + getFlagResultFromFeatureContext(featureContext, identityKey)); + } } } - return new EvaluationResult().withContext(context).withFlags(flags).withSegments(segments); + return new EvaluationResult().withFlags(flags).withSegments(segments); } private static FlagResult getFlagResultFromFeatureContext( @@ -95,7 +102,7 @@ private static FlagResult getFlagResultFromFeatureContext( .withFeatureKey(featureContext.getFeatureKey()) .withName(featureContext.getName()) .withValue(variant.getValue()) - .withReason("SPLIT; weight=" + weight); + .withReason("SPLIT; weight=" + weight.intValue()); } startPercentage = limit; } diff --git a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java index 9d7c326c..892a5c80 100644 --- a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java +++ b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java @@ -1,7 +1,12 @@ 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 @@ -41,6 +46,32 @@ public SegmentCondition(SegmentConditions operator, String property, Object valu 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. * diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 4a59f788..216ef6cf 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -102,7 +102,7 @@ private static Boolean contextMatchesCondition( } } - if (!(contextValue instanceof Boolean)) { + if (!(contextValue instanceof Boolean) && contextValue != null) { contextValue = String.valueOf(contextValue); } diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index acc6ba22..c69f8805 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -5,11 +5,13 @@ import com.flagsmith.exceptions.FeatureNotFoundError; import com.flagsmith.exceptions.FlagsmithClientError; import com.flagsmith.flagengine.EvaluationResult; +import com.flagsmith.flagengine.FlagResult; import com.flagsmith.interfaces.DefaultFlagHandler; import com.flagsmith.threads.AnalyticsProcessor; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.stream.Collectors; import lombok.Data; @@ -22,7 +24,7 @@ public class Flags { /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor */ public static Flags fromFeatureStateModels( @@ -34,7 +36,7 @@ public static Flags fromFeatureStateModels( /** * Build flags object from list of feature states. * - * @param featureStates list of feature states + * @param featureStates list of feature states * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler default flags (optional) */ @@ -60,7 +62,7 @@ public static Flags fromFeatureStateModels( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -88,7 +90,7 @@ public static Flags fromApiFlags( /** * Return the flags instance. * - * @param apiFlags Dictionary with api flags + * @param apiFlags Dictionary with api flags * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -116,7 +118,7 @@ public static Flags fromApiFlags( /** * Build flags object from evaluation result. * - * @param evaluationResult evaluation result + * @param evaluationResult evaluation result * @param analyticsProcessor instance of analytics processor * @param defaultFlagHandler handler for default flags if present */ @@ -124,15 +126,18 @@ public static Flags fromEvaluationResult( EvaluationResult evaluationResult, AnalyticsProcessor analyticsProcessor, DefaultFlagHandler defaultFlagHandler) { - Map flagMap = evaluationResult.getFlags().stream() + Map flagMap = evaluationResult.getFlags().getAdditionalProperties() + .entrySet() + .stream() .collect( Collectors.toMap( - (fs) -> fs.getName(), - (fs) -> { + Entry::getKey, + entry -> { + FlagResult flagResult = entry.getValue(); Flag flag = new Flag(); - flag.setFeatureName(fs.getName()); - flag.setValue(fs.getValue()); - flag.setEnabled(fs.getEnabled()); + flag.setFeatureName(flagResult.getName()); + flag.setValue(flagResult.getValue()); + flag.setEnabled(flagResult.getEnabled()); return flag; })); diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index 4d0162af..fa20de6f 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -1,103 +1,58 @@ package com.flagsmith.flagengine; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.MapperFactory; -import com.flagsmith.mappers.EngineMappers; -import com.flagsmith.models.FeatureStateModel; -import com.flagsmith.models.Flags; - -import groovy.util.Eval; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.BeforeClass; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class EngineTest { - private static final String ENVIRONMENT_JSON_FILE_LOCATION = - "src/test/java/com/flagsmith/flagengine/enginetestdata/" + - "data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"; - private static ObjectMapper objectMapper = MapperFactory.getMapper(); - - private static Stream engineTestData() { - try { - JsonNode engineTestData = objectMapper.readTree(new File(ENVIRONMENT_JSON_FILE_LOCATION)); - - JsonNode environmentDocument = engineTestData.get("environment"); - JsonNode identitiesAndResponses = engineTestData.get("identities_and_responses"); - - EvaluationContext baseEvaluationContext = EngineMappers.mapEnvironmentDocumentToContext(environmentDocument); - - List returnValues = new ArrayList<>(); - - if (identitiesAndResponses.isArray()) { - for (JsonNode identityAndResponse : identitiesAndResponses) { - JsonNode identity = identityAndResponse.get("identity"); - Map traits = Optional.ofNullable(identity.get("identity_traits")) - .filter(JsonNode::isArray) - .map(node -> StreamSupport.stream(node.spliterator(), false) - .filter(trait -> trait.hasNonNull("trait_key")) - .collect(Collectors.toMap( - trait -> trait.get("trait_key").asText(), - trait -> objectMapper.convertValue(trait.get("trait_value"), Object.class)))) - .orElseGet(HashMap::new); - - EvaluationContext evaluationContext = EngineMappers.mapContextAndIdentityDataToContext( - baseEvaluationContext, identity.get("identifier").asText(), traits); - - if (identity.hasNonNull("django_id")) { - evaluationContext.getIdentity().setKey(identity.get("django_id").asText()); - } - - JsonNode expectedResponse = identityAndResponse.get("response"); - - returnValues.add(Arguments.of(evaluationContext, expectedResponse)); - - } - } - - return returnValues.stream(); - - } catch (Exception e) { - System.out.println("Exception in engineTestData: " + e.getMessage()); - e.printStackTrace(); + private static final Path testCasesPath = Paths + .get("src/test/java/com/flagsmith/flagengine/enginetestdata/test_cases"); + private static JsonMapper mapper = JsonMapper.builder() + .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS.mappedFeature()) + .build(); + RecursiveComparisonConfiguration recursiveComparisonConfig = RecursiveComparisonConfiguration.builder() + .build(); + + private static Arguments engineTestDataFromFile(Path path) { + try (BufferedReader reader = Files.newBufferedReader(path)) { + JsonNode root = mapper.readTree(reader); + return Arguments.of( + mapper.treeToValue(root.get("context"), EvaluationContext.class), + mapper.treeToValue(root.get("result"), EvaluationResult.class)); + } catch (IOException e) { + throw new RuntimeException("Failed to read test data from file: " + path, e); } - return null; + } + + private static Stream engineTestData() throws IOException { + return Files.walk(testCasesPath) + .filter(Files::isRegularFile) + .filter(p -> { + String n = p.getFileName().toString(); + return n.endsWith(".json") || n.endsWith(".jsonc"); + }) + .map(EngineTest::engineTestDataFromFile); } @ParameterizedTest() @MethodSource("engineTestData") - public void testEngine(EvaluationContext evaluationContext, JsonNode expectedResponse) { + public void testEngine(EvaluationContext evaluationContext, EvaluationResult expectedResult) { EvaluationResult evaluationResult = Engine.getEvaluationResult(evaluationContext); - List flags = objectMapper.convertValue( - expectedResponse.get("flags"), - new TypeReference>() {}); - - flags.sort((fsm1, fsm2) -> fsm1.getFeature().getName().compareTo(fsm2.getFeature().getName())); - List sortedResults = evaluationResult.getFlags().stream() - .sorted((fr1, fr2) -> fr1.getName().compareTo(fr2.getName())) - .collect(Collectors.toList()); - - assertEquals(flags.size(), sortedResults.size()); - for (int i = 0; i < flags.size(); i++) { - FeatureStateModel fsm = flags.get(i); - FlagResult fr = sortedResults.get(i); - - assertEquals(fsm.getFeature().getName(), fr.getName()); - assertEquals(fsm.getEnabled(), fr.getEnabled()); - assertEquals(fsm.getValue(), fr.getValue()); - } + assertThat(evaluationResult) + .usingRecursiveComparison(recursiveComparisonConfig) + .ignoringAllOverriddenEquals() + .isEqualTo(expectedResult); } } \ No newline at end of file diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index a7847fca..dbf77f12 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit a7847fcaa16cf72f68e88215c690e9399fc0d807 +Subproject commit dbf77f121a346e2c51a3e557a3d0ec2f9cc8c220 From c3a88119a47993db3c3f07dbb6d5977ad3615d4c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 17:35:19 +0100 Subject: [PATCH 26/62] cleanup --- .../flagengine/unit/segments/SegmentEvaluatorTest.java | 1 - .../java/com/flagsmith/threads/AnalyticsProcessorTest.java | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 89e0c4eb..9ce2890c 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/com/flagsmith/threads/AnalyticsProcessorTest.java b/src/test/java/com/flagsmith/threads/AnalyticsProcessorTest.java index e2e3dd2e..6895f4b9 100644 --- a/src/test/java/com/flagsmith/threads/AnalyticsProcessorTest.java +++ b/src/test/java/com/flagsmith/threads/AnalyticsProcessorTest.java @@ -16,20 +16,14 @@ import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.config.Retry; import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.LongAccumulator; -import java.util.concurrent.atomic.LongAdder; import lombok.SneakyThrows; import okhttp3.Response; import okhttp3.mock.MockInterceptor; -import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - public class AnalyticsProcessorTest { private FlagsmithApiWrapper api; From bef6681a8a8c0799bf99b561cf1d995c6521ff3e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 17:38:49 +0100 Subject: [PATCH 27/62] use isEmpty --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 4f74409f..ad204ce8 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -145,7 +145,7 @@ private static Map mapIdentityOverridesToSegments( for (JsonNode identityOverride : identityOverrides) { JsonNode identityFeatures = identityOverride.get("identity_features"); - if (identityFeatures == null || !identityFeatures.isArray() || identityFeatures.size() == 0) { + if (identityFeatures == null || !identityFeatures.isArray() || identityFeatures.isEmpty()) { continue; } From b0ad7d72bdd4f62b6c70c919f9e4ab71d5a94f50 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 17:48:23 +0100 Subject: [PATCH 28/62] improve identity override segments docs --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index ad204ce8..0dda87d4 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -196,7 +196,7 @@ private static Map mapIdentityOverridesToSegments( for (FeatureContext featureContext : overridesKey) { // Copy the feature context for the override FeatureContext override = new FeatureContext(featureContext) - .withKey(""); // Override the key for identity overrides + .withKey(""); // Identity overrides are never multivariate, so no need to set key overrides.add(override); } @@ -289,6 +289,9 @@ private static String getFeatureStateKey(JsonNode featureState) { if (node != null && !node.isNull()) { return node.asText(); } + // Feature state key is used in multivariate feature evaluation, not to + // identify features uniquely, so if both fields are missing, + // we don't need to care about collisions. return ""; } From d12ef3f2493e48f75a8f6bb4d0d447a582341006 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 18:00:32 +0100 Subject: [PATCH 29/62] improve identity overrides segments keys --- .../com/flagsmith/mappers/EngineMappers.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 0dda87d4..a73b619d 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -177,8 +177,7 @@ private static Map mapIdentityOverridesToSegments( List overridesKey = entry.getKey(); List identifiers = entry.getValue(); - // Generate unique segment key - String segmentKey = String.valueOf(overridesKey.hashCode()); + String segmentKey = generateVirtualSegmentKey(overridesKey); // Create segment condition for identifier check SegmentCondition identifierCondition = new SegmentCondition() @@ -191,17 +190,17 @@ private static Map mapIdentityOverridesToSegments( .withType(SegmentRule.Type.ALL) .withConditions(List.of(identifierCondition)); - // Create overrides from FeatureContext objects (much cleaner now!) + // 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 are never multivariate, so no need to set key + .withKey(""); // Identity overrides never carry multivariate options overrides.add(override); } SegmentContext segmentContext = new SegmentContext() - .withKey("") + .withKey("") // Identity override segments never use % Split operator .withName("identity_overrides") .withRules(List.of(segmentRule)) .withOverrides(overrides); @@ -339,7 +338,7 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt for (JsonNode multivariateValue : sortedMultivariate) { FeatureValue variant = new FeatureValue() .withValue(getFeatureStateValue( - multivariateValue.get("multivariate_feature_option"), "value")) + multivariateValue.get("multivariate_feature_option"), "value")) .withWeight(multivariateValue.get("percentage_allocation").asDouble()); variants.add(variant); } @@ -387,4 +386,31 @@ private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { .withRules(rules) .withOverrides(overrides); } + + /** + * 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 generateVirtualSegmentKey( + List 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()); + } } \ No newline at end of file From ec6000e15cf7798e9a00ee29497700274b7877c9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 18:07:16 +0100 Subject: [PATCH 30/62] use stringListTypeRef --- .../com/flagsmith/flagengine/segments/SegmentEvaluator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 216ef6cf..a55e6c01 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -27,6 +27,8 @@ public class SegmentEvaluator { private static Configuration jsonPathConfiguration = Configuration .defaultConfiguration() .setOptions(Option.SUPPRESS_EXCEPTIONS); + private static TypeReference> stringListTypeRef = new TypeReference>() { + }; /** * Check if context is in segment. @@ -94,8 +96,7 @@ private static Boolean contextMatchesCondition( try { // Try parsing a JSON list first conditionList = mapper.readValue( - stringConditionValue, new TypeReference>() { - }); + stringConditionValue, stringListTypeRef); } catch (IOException e) { // As a fallback, split by comma conditionList = Arrays.asList(stringConditionValue.split(",")); From db248ddb5688c2bce909097d5c08194f937cc7fc Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 9 Oct 2025 18:29:23 +0100 Subject: [PATCH 31/62] refactor getEvaluationResult, add priority constants --- .../java/com/flagsmith/flagengine/Engine.java | 47 +++++++++++++++++-- .../flagsmith/flagengine/EngineConstants.java | 6 +++ .../com/flagsmith/mappers/EngineMappers.java | 3 +- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/flagsmith/flagengine/EngineConstants.java diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index 3780038a..5e29f863 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -8,6 +8,26 @@ import org.apache.commons.lang3.tuple.ImmutablePair; public class Engine { + private static class SegmentEvaluationResult { + List segments; + HashMap> segmentFeatureContexts; + + public SegmentEvaluationResult( + List segments, + HashMap> segmentFeatureContexts) { + this.segments = segments; + this.segmentFeatureContexts = segmentFeatureContexts; + } + + public List getSegments() { + return segments; + } + + public HashMap> getSegmentFeatureContexts() { + return segmentFeatureContexts; + } + } + /** * Get evaluation result for a given evaluation context. * @@ -15,9 +35,18 @@ public class Engine { * @return Evaluation result. */ public static EvaluationResult getEvaluationResult(EvaluationContext context) { + SegmentEvaluationResult segmentEvaluationResult = evaluateSegments(context); + Flags flags = evaluateFeatures(context, segmentEvaluationResult.getSegmentFeatureContexts()); + + return new EvaluationResult() + .withFlags(flags) + .withSegments(segmentEvaluationResult.getSegments()); + } + + private static SegmentEvaluationResult evaluateSegments( + EvaluationContext context) { List segments = new ArrayList<>(); HashMap> segmentFeatureContexts = new HashMap<>(); - Flags flags = new Flags(); for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { @@ -36,10 +65,10 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { FeatureContext existingFeatureContext = existing.getRight(); Double existingPriority = existingFeatureContext.getPriority() == null - ? Double.POSITIVE_INFINITY + ? EngineConstants.WEAKEST_PRIORITY : existingFeatureContext.getPriority(); Double featurePriority = featureContext.getPriority() == null - ? Double.POSITIVE_INFINITY + ? EngineConstants.WEAKEST_PRIORITY : featureContext.getPriority(); if (existingPriority < featurePriority) { @@ -54,11 +83,19 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { } } + return new SegmentEvaluationResult(segments, segmentFeatureContexts); + } + + private static Flags evaluateFeatures( + EvaluationContext context, + HashMap> segmentFeatureContexts) { + Features contextFeatures = context.getFeatures(); + Flags flags = new Flags(); + String identityKey = context.getIdentity() != null ? context.getIdentity().getKey() : null; - Features contextFeatures = context.getFeatures(); if (contextFeatures != null) { for (FeatureContext featureContext : contextFeatures.getAdditionalProperties().values()) { if (segmentFeatureContexts.containsKey(featureContext.getFeatureKey())) { @@ -80,7 +117,7 @@ public static EvaluationResult getEvaluationResult(EvaluationContext context) { } } - return new EvaluationResult().withFlags(flags).withSegments(segments); + return flags; } private static FlagResult getFlagResultFromFeatureContext( 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/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index a73b619d..93a3dbbc 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -1,6 +1,7 @@ package com.flagsmith.mappers; import com.fasterxml.jackson.databind.JsonNode; +import com.flagsmith.flagengine.EngineConstants; import com.flagsmith.flagengine.EnvironmentContext; import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.flagengine.FeatureContext; @@ -164,7 +165,7 @@ private static Map mapIdentityOverridesToSegments( .withName(feature.get("name").asText()) .withEnabled(featureState.get("enabled").asBoolean()) .withValue(getFeatureStateValue(featureState, "feature_state_value")) - .withPriority(Double.NEGATIVE_INFINITY); // Highest possible priority + .withPriority(EngineConstants.STRONGEST_PRIORITY); overridesKey.add(featureContext); } From c4d35e2920028cf2b1af16290a41ac1b345ad82c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 10 Oct 2025 12:57:19 +0100 Subject: [PATCH 32/62] improve docs --- .../java/com/flagsmith/flagengine/IdentityContext.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/flagsmith/flagengine/IdentityContext.java b/src/main/java/com/flagsmith/flagengine/IdentityContext.java index 73b10252..62329f3c 100644 --- a/src/main/java/com/flagsmith/flagengine/IdentityContext.java +++ b/src/main/java/com/flagsmith/flagengine/IdentityContext.java @@ -1,3 +1,12 @@ +/* + * 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; From 0e827fc12dca50a8a045d6c8099fa549c07ef9e9 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 10 Oct 2025 13:02:01 +0100 Subject: [PATCH 33/62] improve readability --- .../flagengine/segments/SegmentEvaluator.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index a55e6c01..9e336747 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -110,13 +110,15 @@ private static Boolean contextMatchesCondition( return conditionList.contains(contextValue); case PERCENTAGE_SPLIT: - String key = (contextValue != null) ? contextValue.toString() - : (context.getIdentity() != null) ? context.getIdentity().getKey() : null; - - if (key == null) { - return false; + 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); final float floatValue; From 3819cd99615421206a37922f469e7026c3bc72f6 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 11:38:03 +0100 Subject: [PATCH 34/62] Support segment metadata --- .../java/com/flagsmith/FlagsmithClient.java | 21 +++++- .../java/com/flagsmith/flagengine/Engine.java | 3 +- .../com/flagsmith/mappers/EngineMappers.java | 30 +++++++-- .../com/flagsmith/models/SegmentMetadata.java | 65 +++++++++++++++++++ .../com/flagsmith/FlagsmithClientTest.java | 39 +++++++++++ 5 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/flagsmith/models/SegmentMetadata.java diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 2c969fc5..29c6e17e 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -1,5 +1,6 @@ package com.flagsmith; +import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.config.FlagsmithCacheConfig; import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.exceptions.FlagsmithApiError; @@ -14,11 +15,13 @@ import com.flagsmith.models.BaseFlag; import com.flagsmith.models.Flags; import com.flagsmith.models.Segment; +import com.flagsmith.models.SegmentMetadata; import com.flagsmith.threads.PollingManager; import com.flagsmith.utils.ModelUtils; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import lombok.Data; @@ -171,12 +174,26 @@ public List getIdentitySegments(String identifier, Map final EvaluationResult result = Engine.getEvaluationResult(context); return result.getSegments().stream().map((segmentModel) -> { + if (segmentModel.getMetadata() == null) { + return null; + } + + ObjectMapper mapper = MapperFactory.getMapper(); + SegmentMetadata segmentMetadata = mapper.convertValue( + segmentModel.getMetadata(), SegmentMetadata.class); + + Integer flagsmithId = segmentMetadata.getFlagsmithId(); + if (segmentMetadata.getSource() != SegmentMetadata.Source.API + || flagsmithId == null) { + return null; + } + Segment segment = new Segment(); - segment.setId(Integer.valueOf(segmentModel.getKey())); + segment.setId(flagsmithId); segment.setName(segmentModel.getName()); return segment; - }).collect(Collectors.toList()); + }).filter(Objects::nonNull).collect(Collectors.toList()); } /** diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index 5e29f863..a18edcb2 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -51,7 +51,8 @@ private static SegmentEvaluationResult evaluateSegments( for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { segments.add(new SegmentResult().withKey(segmentContext.getKey()) - .withName(segmentContext.getName())); + .withName(segmentContext.getName()) + .withMetadata(segmentContext.getMetadata())); List segmentOverrides = segmentContext.getOverrides(); diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 93a3dbbc..6971abf0 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -1,18 +1,21 @@ 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.IdentityContext; +import com.flagsmith.flagengine.Metadata; 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.SegmentMetadata; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -200,11 +203,20 @@ private static Map mapIdentityOverridesToSegments( 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>() { + }); + SegmentContext segmentContext = new SegmentContext() .withKey("") // Identity override segments never use % Split operator .withName("identity_overrides") .withRules(List.of(segmentRule)) - .withOverrides(overrides); + .withOverrides(overrides) + .withMetadata(metadataMap); segmentContexts.put(segmentKey, segmentContext); } @@ -365,8 +377,6 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt * @return the segment context */ private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { - String segmentKey = segment.get("id").asText(); - // Map rules List rules = new ArrayList<>(); JsonNode segmentRules = segment.get("rules"); @@ -381,11 +391,23 @@ private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts(segmentFeatureStates); } + // Map metadata + SegmentMetadata metadata = new SegmentMetadata(); + metadata.setSource(SegmentMetadata.Source.API); + metadata.setFlagsmithId(segment.get("id").asInt()); + + Map metadataMap = MapperFactory.getMapper() + .convertValue(metadata, + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + + String segmentKey = segment.get("id").asText(); return new SegmentContext() .withKey(segmentKey) .withName(segment.get("name").asText()) .withRules(rules) - .withOverrides(overrides); + .withOverrides(overrides) + .withMetadata(metadataMap); } /** diff --git a/src/main/java/com/flagsmith/models/SegmentMetadata.java b/src/main/java/com/flagsmith/models/SegmentMetadata.java new file mode 100644 index 00000000..8f26b304 --- /dev/null +++ b/src/main/java/com/flagsmith/models/SegmentMetadata.java @@ -0,0 +1,65 @@ +package com.flagsmith.models; + +/** + * SegmentMetadata + * + *

Additional metadata associated with a segment. + * + */ +public class SegmentMetadata { + /** + * Source + * + *

How the segment was created. + * If the segment was created via the API, this will be `API`. + * If the segment was created to support identity overrides in local evaluation, + * this will be `IDENTITY_OVERRIDES`. + */ + public enum Source { + API, + IDENTITY_OVERRIDES; + } + + private Integer flagsmithId; + private Source source; + + /* + * FlagsmithId + *

The internal Flagsmith ID for the segment. + * + * @return The flagsmithId + */ + public Integer getFlagsmithId() { + return flagsmithId; + } + + /* + * FlagsmithId + *

The internal Flagsmith ID for the segment. + * + * @param flagsmithId The flagsmithId + */ + public void setFlagsmithId(Integer flagsmithId) { + this.flagsmithId = flagsmithId; + } + + /* + * Source + *

How the segment was created. + * + * @return The source + */ + public Source getSource() { + return source; + } + + /* + * Source + *

How the segment was created. + * + * @param source The source + */ + public void setSource(Source source) { + this.source = source; + } +} diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 07404ea4..89d40ac6 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -543,6 +543,45 @@ public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingExcepti assertEquals(segments.get(0).getName(), "Test segment"); } + @Test + public void testGetIdentitySegments__NonAPISourceInMetadata__ReturnsExpected() throws JsonProcessingException, + FlagsmithClientError { + String baseUrl = "http://bad-url"; + + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + FlagsmithTestHelper.environmentString(), + MEDIATYPE_JSON); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .withLocalEvaluation(true) + .build()) + .setApiKey("ser.abcdefg") + .build(); + + client.updateEnvironment(); + + String identifier = "overridden-identity"; + Map traits = new HashMap() { + { + put("foo", "bar"); + } + }; + + List segments = client.getIdentitySegments(identifier, traits); + + // no identity overrides segment present + assertEquals(segments.size(), 1); + assertEquals(segments.get(0).getName(), "Test segment"); + } + @Test public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { // Given From de0362b049b64c9fc0186503beef88f2a2f055df Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 11:38:12 +0100 Subject: [PATCH 35/62] improve naming --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 6971abf0..9abcd2ed 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -181,7 +181,7 @@ private static Map mapIdentityOverridesToSegments( List overridesKey = entry.getKey(); List identifiers = entry.getValue(); - String segmentKey = generateVirtualSegmentKey(overridesKey); + String segmentKey = getVirtualSegmentKey(overridesKey); // Create segment condition for identifier check SegmentCondition identifierCondition = new SegmentCondition() @@ -418,7 +418,7 @@ private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { * @param featureContexts list of feature contexts * @return unique segment key */ - private static String generateVirtualSegmentKey( + private static String getVirtualSegmentKey( List featureContexts) { StringBuilder keyBuilder = new StringBuilder(); From b8ae5abd88522070c7ac23f37fd5c3b181f894b4 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 11:57:56 +0100 Subject: [PATCH 36/62] support variant priority --- .../java/com/flagsmith/flagengine/Engine.java | 8 ++++++- .../com/flagsmith/mappers/EngineMappers.java | 24 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index a18edcb2..8f61cc4a 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -132,7 +132,13 @@ private static FlagResult getFlagResultFromFeatureContext( Float startPercentage = 0.0f; - for (FeatureValue variant : variants) { + ArrayList 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) { diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 9abcd2ed..15afe16e 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * EngineMappers @@ -305,6 +306,21 @@ private static String getFeatureStateKey(JsonNode featureState) { // identify features uniquely, so if both fields are missing, // we don't need to care about collisions. return ""; + + private static double getMultivariateFeatureValuePriority(JsonNode multivariateValue) { + JsonNode idNode = multivariateValue.get("id"); + if (idNode != null && !idNode.isNull()) { + return idNode.asDouble(); + } + // Fallback to mv_fs_value_uuid if id is not present + JsonNode mvFsValueUuidNode = multivariateValue.get("mv_fs_value_uuid"); + if (mvFsValueUuidNode != null && !mvFsValueUuidNode.isNull()) { + UUID mvFsValueUuid = UUID.fromString(mvFsValueUuidNode.asText()); + return mvFsValueUuid.getMostSignificantBits() & Long.MAX_VALUE; + } + + throw new IllegalArgumentException( + "Multivariate feature value must have either 'id' or 'mv_fs_value_uuid'"); } private static Object getFeatureStateValue(JsonNode featureState, String fieldName) { @@ -345,14 +361,12 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); if (multivariateValues != null && multivariateValues.isArray()) { List variants = new ArrayList<>(); - List sortedMultivariate = new ArrayList<>(); - multivariateValues.forEach(sortedMultivariate::add); - sortedMultivariate.sort((a, b) -> a.get("id").asText().compareTo(b.get("id").asText())); - for (JsonNode multivariateValue : sortedMultivariate) { + for (JsonNode multivariateValue : multivariateValues) { FeatureValue variant = new FeatureValue() .withValue(getFeatureStateValue( multivariateValue.get("multivariate_feature_option"), "value")) - .withWeight(multivariateValue.get("percentage_allocation").asDouble()); + .withWeight(multivariateValue.get("percentage_allocation").asDouble()) + .withPriority(getMultivariateFeatureValuePriority(multivariateValue)); variants.add(variant); } featureContext.withVariants(variants); From 7764097643eec33431550242cc1636d68a415d37 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 11:58:02 +0100 Subject: [PATCH 37/62] prevent illegal state --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 15afe16e..4dac6dbf 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -302,10 +302,10 @@ private static String getFeatureStateKey(JsonNode featureState) { if (node != null && !node.isNull()) { return node.asText(); } - // Feature state key is used in multivariate feature evaluation, not to - // identify features uniquely, so if both fields are missing, - // we don't need to care about collisions. - return ""; + + throw new IllegalArgumentException( + "Feature state must have either 'django_id' or 'featurestate_uuid'"); + } private static double getMultivariateFeatureValuePriority(JsonNode multivariateValue) { JsonNode idNode = multivariateValue.get("id"); From 7303aebbdc5cf2939837706ca4550d64f7225edb Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:13:21 +0100 Subject: [PATCH 38/62] clarify context value getter behaviour --- .../flagengine/segments/SegmentEvaluator.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index 9e336747..b98dd5db 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -190,14 +190,22 @@ private static Boolean contextMatchesCondition( * @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("$.")) { - return JsonPath + result = JsonPath .using(jsonPathConfiguration) .parse(mapper.convertValue(context, Map.class)) .read(property); - } - if (context.getIdentity() != null) { - return context.getIdentity().getTraits().getAdditionalProperties().get(property); + if (result instanceof List || result instanceof Map) { + return null; + } + return result; } return null; } From c37164bba98ab5c1d8fe6a163104cb36b89f52c6 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:17:58 +0100 Subject: [PATCH 39/62] fix type casting --- .../com/flagsmith/flagengine/utils/types/TypeCasting.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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..73877e60 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)); } /** @@ -151,8 +151,8 @@ 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))); } /** From 50b4bec4e921e67edddb8889028674d47a17f3ca Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:36:00 +0100 Subject: [PATCH 40/62] fix boolean coercion --- .../com/flagsmith/flagengine/utils/types/TypeCasting.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 73877e60..9b05b2f3 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -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; } } @@ -152,7 +152,8 @@ public static Boolean toBoolean(Object str) { public static Boolean isBoolean(Object str) { return str instanceof Boolean || Boolean.TRUE.toString().equalsIgnoreCase((String.valueOf(str))) - || Boolean.FALSE.toString().equalsIgnoreCase((String.valueOf(str))); + || Boolean.FALSE.toString().equalsIgnoreCase((String.valueOf(str))) + || "1".equals(String.valueOf(str)); } /** From b4ee7ff72996fd8610abd2951b919c7ce137b4ec Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:36:19 +0100 Subject: [PATCH 41/62] add test names --- pom.xml | 2 +- src/test/java/com/flagsmith/flagengine/EngineTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0d7efab8..7890d487 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ 1.18.34 1.7.30 3.4.0 - 5.9.2 + 5.14.0 diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index fa20de6f..e99a2fa4 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -27,7 +27,8 @@ public class EngineTest { private static Arguments engineTestDataFromFile(Path path) { try (BufferedReader reader = Files.newBufferedReader(path)) { JsonNode root = mapper.readTree(reader); - return Arguments.of( + return Arguments.argumentSet( + path.getFileName().toString(), mapper.treeToValue(root.get("context"), EvaluationContext.class), mapper.treeToValue(root.get("result"), EvaluationResult.class)); } catch (IOException e) { From 540832fd77fa4ff740c66e4671fe46277753df6b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:36:55 +0100 Subject: [PATCH 42/62] use latest test data --- .gitmodules | 2 +- src/test/java/com/flagsmith/flagengine/enginetestdata | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index b3c2502a..2e4b1ea7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"] path = src/test/java/com/flagsmith/flagengine/enginetestdata url = git@github.com:Flagsmith/engine-test-data.git - branch = fix/schema-errors + tag = v2.4.0 \ No newline at end of file diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index dbf77f12..6453b039 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit dbf77f121a346e2c51a3e557a3d0ec2f9cc8c220 +Subproject commit 6453b0391344a4d677a97cc4a9d27a8b8e329787 From 60d5d9e1e75d7baf03eecc5722fa4d8ff1ed2083 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 12:47:09 +0100 Subject: [PATCH 43/62] cleanup --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 4dac6dbf..13b0cb4c 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -8,7 +8,6 @@ import com.flagsmith.flagengine.FeatureContext; import com.flagsmith.flagengine.FeatureValue; import com.flagsmith.flagengine.IdentityContext; -import com.flagsmith.flagengine.Metadata; import com.flagsmith.flagengine.SegmentCondition; import com.flagsmith.flagengine.SegmentContext; import com.flagsmith.flagengine.SegmentRule; From e7cfad59c0cd6f8b049633d5c1b6ced1279824a3 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 18:02:13 +0100 Subject: [PATCH 44/62] bump maven-surefire-plugin --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7890d487..2f4851e4 100644 --- a/pom.xml +++ b/pom.xml @@ -292,7 +292,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.0 + 3.5.4 From c5821d960219d73185488b8fd45749e38a88b9bb Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 18:21:08 +0100 Subject: [PATCH 45/62] bring models back --- .../environments/EnvironmentModel.java | 28 ++ .../flagengine/features/FeatureModel.java | 10 + .../features/FeatureSegmentModel.java | 9 + .../features/FeatureStateModel.java | 23 ++ .../MultivariateFeatureOptionModel.java | 9 + .../MultivariateFeatureStateValueModel.java | 17 ++ .../flagengine/identities/IdentityModel.java | 23 ++ .../flagengine/projects/ProjectModel.java | 11 + .../segments/SegmentConditionModel.java | 13 + .../flagengine/segments/SegmentModel.java | 16 ++ .../flagengine/segments/SegmentRuleModel.java | 11 + .../com/flagsmith/mappers/EngineMappers.java | 242 ++++++++---------- 12 files changed, 276 insertions(+), 136 deletions(-) create mode 100644 src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java create mode 100644 src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java diff --git a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java b/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java new file mode 100644 index 00000000..912dec42 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java @@ -0,0 +1,28 @@ +package com.flagsmith.flagengine.environments; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.flagengine.identities.IdentityModel; +import com.flagsmith.flagengine.projects.ProjectModel; +import com.flagsmith.utils.models.BaseModel; +import java.util.List; +import lombok.Data; + +@Data +public class EnvironmentModel extends BaseModel { + private Integer id; + + @JsonProperty("api_key") + private String apiKey; + + @JsonProperty("name") + private String name; + + private ProjectModel project; + + @JsonProperty("feature_states") + private List featureStates; + + @JsonProperty("identity_overrides") + private List identityOverrides; +} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java new file mode 100644 index 00000000..7285d105 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java @@ -0,0 +1,10 @@ +package com.flagsmith.flagengine.features; + +import lombok.Data; + +@Data +public class FeatureModel { + private Integer id; + private String name; + private String type; +} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java new file mode 100644 index 00000000..f7a55d88 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java @@ -0,0 +1,9 @@ +package com.flagsmith.flagengine.features; + +import com.flagsmith.flagengine.utils.models.BaseModel; +import lombok.Data; + +@Data +public class FeatureSegmentModel extends BaseModel { + private Integer priority; +} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java new file mode 100644 index 00000000..1057e937 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java @@ -0,0 +1,23 @@ +package com.flagsmith.flagengine.features; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.utils.models.BaseModel; +import java.util.List; +import java.util.UUID; +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; +} diff --git a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java new file mode 100644 index 00000000..e5b95317 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java @@ -0,0 +1,9 @@ +package com.flagsmith.flagengine.features; + +import com.flagsmith.utils.models.BaseModel; +import lombok.Data; + +@Data +public class MultivariateFeatureOptionModel extends BaseModel { + private String value; +} diff --git a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java new file mode 100644 index 00000000..853129c9 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java @@ -0,0 +1,17 @@ +package com.flagsmith.flagengine.features; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.utils.models.BaseModel; +import java.util.UUID; +import lombok.Data; + +@Data +public class MultivariateFeatureStateValueModel extends BaseModel { + @JsonProperty("multivariate_feature_option") + private MultivariateFeatureOptionModel multivariateFeatureOption; + @JsonProperty("percentage_allocation") + private Float percentageAllocation; + private Integer id; + @JsonProperty("mv_fs_value_uuid") + private String mvFsValueUuid = UUID.randomUUID().toString(); +} diff --git a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java new file mode 100644 index 00000000..366b4685 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java @@ -0,0 +1,23 @@ +package com.flagsmith.flagengine.identities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.utils.models.BaseModel; +import java.sql.Date; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.Data; + +@Data +public class IdentityModel extends BaseModel { + @JsonProperty("django_id") + private Integer djangoId; + private String identifier; + @JsonProperty("created_date") + private Date createdDate; + @JsonProperty("identity_uuid") + private String identityUuid = UUID.randomUUID().toString(); + @JsonProperty("identity_features") + private List identityFeatures = new ArrayList<>(); +} diff --git a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java b/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java new file mode 100644 index 00000000..6924fa4d --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java @@ -0,0 +1,11 @@ +package com.flagsmith.flagengine.projects; + +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 List segments; +} diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java new file mode 100644 index 00000000..aa7ec45f --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java @@ -0,0 +1,13 @@ +package com.flagsmith.flagengine.segments; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import lombok.Data; + +@Data +public class SegmentConditionModel { + private SegmentConditions operator; + private String value; + @JsonProperty("property_") + private String property; +} diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java new file mode 100644 index 00000000..3ec15fef --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java @@ -0,0 +1,16 @@ +package com.flagsmith.flagengine.segments; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.utils.models.BaseModel; +import java.util.List; +import lombok.Data; + +@Data +public class SegmentModel extends BaseModel { + private Integer id; + private String name; + private List rules; + @JsonProperty("feature_states") + private List featureStates; +} diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java new file mode 100644 index 00000000..08977e29 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java @@ -0,0 +1,11 @@ +package com.flagsmith.flagengine.segments; + +import java.util.List; +import lombok.Data; + +@Data +public class SegmentRuleModel { + private String type; + private List rules; + private List conditions; +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 13b0cb4c..dbfad027 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -13,6 +13,16 @@ import com.flagsmith.flagengine.SegmentRule; import com.flagsmith.flagengine.Segments; import com.flagsmith.flagengine.Traits; +import com.flagsmith.flagengine.environments.EnvironmentModel; +import com.flagsmith.flagengine.features.FeatureModel; +import com.flagsmith.flagengine.features.FeatureSegmentModel; +import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.flagengine.features.MultivariateFeatureStateValueModel; +import com.flagsmith.flagengine.identities.IdentityModel; +import com.flagsmith.flagengine.projects.ProjectModel; +import com.flagsmith.flagengine.segments.SegmentConditionModel; +import com.flagsmith.flagengine.segments.SegmentModel; +import com.flagsmith.flagengine.segments.SegmentRuleModel; import com.flagsmith.flagengine.segments.constants.SegmentConditions; import com.flagsmith.models.SegmentMetadata; import java.util.ArrayList; @@ -74,45 +84,45 @@ public static EvaluationContext mapContextAndIdentityDataToContext( */ 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(environmentDocument.get("api_key").require().asText()) - .withName(environmentDocument.get("name").require().asText()); + .withKey(environmentModel.getApiKey()) + .withName(environmentModel.getName()); // Map features Map features = new HashMap<>(); - JsonNode featureStates = environmentDocument.get("feature_states"); - if (featureStates != null && featureStates.isArray()) { - for (JsonNode featureState : featureStates) { - FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); - features.put(featureContext.getName(), featureContext); - } + for (FeatureStateModel featureState : environmentModel.getFeatureStates()) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + features.put(featureContext.getName(), featureContext); } // Map segments Map segments = new HashMap<>(); // Map project segments - JsonNode project = environmentDocument.get("project"); - if (project != null) { - JsonNode projectSegments = project.get("segments"); - if (projectSegments != null && projectSegments.isArray()) { - for (JsonNode segment : projectSegments) { - String segmentKey = segment.get("id").asText(); - SegmentContext segmentContext = mapSegmentToSegmentContext(segment); - segments.put(segmentKey, segmentContext); - } - } + ProjectModel project = environmentModel.getProject(); + for (SegmentModel segment : project.getSegments()) { + String segmentKey = String.valueOf(segment.getId()); + segments.put(segmentKey, mapSegmentToSegmentContext(segment)); } // Map identity overrides - JsonNode identityOverrides = environmentDocument.get("identity_overrides"); - if (identityOverrides != null && identityOverrides.isArray()) { - Map identityOverrideSegments = mapIdentityOverridesToSegments( - identityOverrides); - segments.putAll(identityOverrideSegments); - } + Map identityOverrideSegments = mapIdentityOverridesToSegments( + environmentModel.getIdentityOverrides()); + segments.putAll(identityOverrideSegments); // Create evaluation context EvaluationContext evaluationContext = new EvaluationContext() @@ -142,37 +152,37 @@ public static EvaluationContext mapEnvironmentDocumentToContext( * @return map of segment contexts */ private static Map mapIdentityOverridesToSegments( - JsonNode identityOverrides) { + List identityOverrides) { // Map from sorted list of feature contexts to identifiers Map, List> featuresToIdentifiers = new HashMap<>(); - for (JsonNode identityOverride : identityOverrides) { - JsonNode identityFeatures = identityOverride.get("identity_features"); - if (identityFeatures == null || !identityFeatures.isArray() || identityFeatures.isEmpty()) { + 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<>(); + List sortedFeatures = new ArrayList<>(); identityFeatures.forEach(sortedFeatures::add); - sortedFeatures.sort((a, b) -> a.get("feature").get("name").asText() - .compareTo(b.get("feature").get("name").asText())); + sortedFeatures.sort((a, b) -> a.getFeature().getName() + .compareTo(b.getFeature().getName())); - for (JsonNode featureState : sortedFeatures) { - JsonNode feature = featureState.get("feature"); + for (FeatureStateModel featureState : sortedFeatures) { + FeatureModel feature = featureState.getFeature(); FeatureContext featureContext = new FeatureContext() .withKey("") - .withFeatureKey(feature.get("id").asText()) - .withName(feature.get("name").asText()) - .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(getFeatureStateValue(featureState, "feature_state_value")) + .withFeatureKey(String.valueOf(feature.getId())) + .withName(feature.getName()) + .withEnabled(featureState.getEnabled()) + .withValue(featureState.getValue()) .withPriority(EngineConstants.STRONGEST_PRIORITY); overridesKey.add(featureContext); } - String identifier = identityOverride.get("identifier").asText(); + String identifier = identityOverride.getIdentifier(); featuresToIdentifiers.computeIfAbsent(overridesKey, k -> new ArrayList<>()).add(identifier); } @@ -231,33 +241,30 @@ private static Map mapIdentityOverridesToSegments( * @return list of segment rules */ private static List mapEnvironmentDocumentRulesToContextRules( - JsonNode rules) { + List rules) { List segmentRules = new ArrayList<>(); - for (JsonNode rule : rules) { + for (SegmentRuleModel rule : rules) { // Map conditions List conditions = new ArrayList<>(); - JsonNode ruleConditions = rule.get("conditions"); - if (ruleConditions != null && ruleConditions.isArray()) { - for (JsonNode condition : ruleConditions) { + + if (rule.getConditions() != null) { + for (SegmentConditionModel condition : rule.getConditions()) { SegmentCondition segmentCondition = new SegmentCondition() - .withProperty(condition.get("property_").asText()) - .withOperator(SegmentConditions.valueOf(condition.get("operator").asText())) - .withValue(condition.get("value").asText()); + .withProperty(condition.getProperty()) + .withOperator(condition.getOperator()) + .withValue(condition.getValue()); conditions.add(segmentCondition); } } // Map sub-rules recursively - List subRules = new ArrayList<>(); - JsonNode ruleRules = rule.get("rules"); - if (ruleRules != null && ruleRules.isArray()) { - subRules = mapEnvironmentDocumentRulesToContextRules(ruleRules); - } + List subRules = mapEnvironmentDocumentRulesToContextRules( + rule.getRules()); SegmentRule segmentRule = new SegmentRule() - .withType(SegmentRule.Type.valueOf(rule.get("type").asText())) + .withType(SegmentRule.Type.fromValue(rule.getType())) .withConditions(conditions) .withRules(subRules); @@ -274,13 +281,15 @@ private static List mapEnvironmentDocumentRulesToContextRules( * @return list of feature contexts */ private static List mapEnvironmentDocumentFeatureStatesToFeatureContexts( - JsonNode featureStates) { + List featureStates) { List featureContexts = new ArrayList<>(); - for (JsonNode featureState : featureStates) { - FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); - featureContexts.add(featureContext); + if (featureStates != null) { + for (FeatureStateModel featureState : featureStates) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + featureContexts.add(featureContext); + } } return featureContexts; @@ -292,52 +301,23 @@ private static List mapEnvironmentDocumentFeatureStatesToFeature * @param featureState the feature state JSON * @return the feature state key as string */ - private static String getFeatureStateKey(JsonNode featureState) { - JsonNode node = featureState.get("django_id"); - if (node != null && !node.isNull()) { - return node.asText(); - } - node = featureState.get("featurestate_uuid"); - if (node != null && !node.isNull()) { - return node.asText(); + private static String getFeatureStateKey(FeatureStateModel featureState) { + Integer djangoId = featureState.getDjangoId(); + if (djangoId != null) { + return djangoId.toString(); } - - throw new IllegalArgumentException( - "Feature state must have either 'django_id' or 'featurestate_uuid'"); + return featureState.getFeaturestateUuid(); } - private static double getMultivariateFeatureValuePriority(JsonNode multivariateValue) { - JsonNode idNode = multivariateValue.get("id"); - if (idNode != null && !idNode.isNull()) { - return idNode.asDouble(); + private static double getMultivariateFeatureValuePriority( + MultivariateFeatureStateValueModel multivariateValue) { + if (multivariateValue.getId() != null) { + return multivariateValue.getId(); } - // Fallback to mv_fs_value_uuid if id is not present - JsonNode mvFsValueUuidNode = multivariateValue.get("mv_fs_value_uuid"); - if (mvFsValueUuidNode != null && !mvFsValueUuidNode.isNull()) { - UUID mvFsValueUuid = UUID.fromString(mvFsValueUuidNode.asText()); - return mvFsValueUuid.getMostSignificantBits() & Long.MAX_VALUE; - } - - throw new IllegalArgumentException( - "Multivariate feature value must have either 'id' or 'mv_fs_value_uuid'"); - } - private static Object getFeatureStateValue(JsonNode featureState, String fieldName) { - JsonNode valueNode = featureState.get(fieldName); - if (valueNode.isTextual() || valueNode.isLong()) { - return valueNode.asText(); - } else if (valueNode.isNumber()) { - if (valueNode.isInt()) { - return valueNode.asInt(); - } else { - return valueNode.asDouble(); - } - } else if (valueNode.isBoolean()) { - return valueNode.asBoolean(); - } else if (valueNode.isArray() || valueNode.isObject()) { - return valueNode; - } - return null; + // Fallback to mv_fs_value_uuid if id is not present + UUID mvFsValueUuid = UUID.fromString(multivariateValue.getMvFsValueUuid()); + return mvFsValueUuid.getMostSignificantBits() & Long.MAX_VALUE; } /** @@ -346,37 +326,32 @@ private static Object getFeatureStateValue(JsonNode featureState, String fieldNa * @param featureState the feature state JSON * @return the feature context */ - private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureState) { - JsonNode feature = featureState.get("feature"); - + private static FeatureContext mapFeatureStateToFeatureContext(FeatureStateModel featureState) { FeatureContext featureContext = new FeatureContext() .withKey(getFeatureStateKey(featureState)) - .withFeatureKey(feature.get("id").asText()) - .withName(feature.get("name").asText()) - .withEnabled(featureState.get("enabled").asBoolean()) - .withValue(getFeatureStateValue(featureState, "feature_state_value")); + .withFeatureKey(String.valueOf(featureState.getFeature().getId())) + .withName(featureState.getFeature().getName()) + .withEnabled(featureState.getEnabled()) + .withValue(featureState.getValue()); // Handle multivariate feature state values - JsonNode multivariateValues = featureState.get("multivariate_feature_state_values"); - if (multivariateValues != null && multivariateValues.isArray()) { - List variants = new ArrayList<>(); - for (JsonNode multivariateValue : multivariateValues) { - FeatureValue variant = new FeatureValue() - .withValue(getFeatureStateValue( - multivariateValue.get("multivariate_feature_option"), "value")) - .withWeight(multivariateValue.get("percentage_allocation").asDouble()) - .withPriority(getMultivariateFeatureValuePriority(multivariateValue)); - variants.add(variant); - } - featureContext.withVariants(variants); + List 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 - JsonNode featureSegment = featureState.get("feature_segment"); - if (featureSegment != null && !featureSegment.isNull()) { - JsonNode priority = featureSegment.get("priority"); - if (priority != null && !priority.isNull()) { - featureContext.withPriority(priority.asDouble()); + FeatureSegmentModel featureSegment = featureState.getFeatureSegment(); + if (featureSegment != null) { + Double priority = (double) featureSegment.getPriority(); + if (priority != null) { + featureContext.withPriority(priority); } } @@ -389,35 +364,30 @@ private static FeatureContext mapFeatureStateToFeatureContext(JsonNode featureSt * @param segment the segment JSON * @return the segment context */ - private static SegmentContext mapSegmentToSegmentContext(JsonNode segment) { + private static SegmentContext mapSegmentToSegmentContext(SegmentModel segment) { // Map rules - List rules = new ArrayList<>(); - JsonNode segmentRules = segment.get("rules"); - if (segmentRules != null && segmentRules.isArray()) { - rules = mapEnvironmentDocumentRulesToContextRules(segmentRules); - } + List rules = mapEnvironmentDocumentRulesToContextRules( + segment.getRules()); // Map overrides - List overrides = new ArrayList<>(); - JsonNode segmentFeatureStates = segment.get("feature_states"); - if (segmentFeatureStates != null && segmentFeatureStates.isArray()) { - overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts(segmentFeatureStates); - } + List segmentFeatureStates = segment.getFeatureStates(); + List overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts( + segmentFeatureStates); // Map metadata SegmentMetadata metadata = new SegmentMetadata(); metadata.setSource(SegmentMetadata.Source.API); - metadata.setFlagsmithId(segment.get("id").asInt()); + metadata.setFlagsmithId(segment.getId()); Map metadataMap = MapperFactory.getMapper() - .convertValue(metadata, + .convertValue(metadata, new com.fasterxml.jackson.core.type.TypeReference>() { }); - String segmentKey = segment.get("id").asText(); + String segmentKey = String.valueOf(segment.getId()); return new SegmentContext() .withKey(segmentKey) - .withName(segment.get("name").asText()) + .withName(segment.getName()) .withRules(rules) .withOverrides(overrides) .withMetadata(metadataMap); From 711ff13f98431017b3f26ee0a51235f7c62ac28f Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 18:26:15 +0100 Subject: [PATCH 46/62] ...but they don't belong to the engine anymore --- .../com/flagsmith/mappers/EngineMappers.java | 20 +++++++++---------- .../environments/EnvironmentModel.java | 10 +++++----- .../features/FeatureModel.java | 4 ++-- .../features/FeatureSegmentModel.java | 6 +++--- .../features/FeatureStateModel.java | 4 ++-- .../MultivariateFeatureOptionModel.java | 4 ++-- .../MultivariateFeatureStateValueModel.java | 4 ++-- .../identities/IdentityModel.java | 6 +++--- .../projects/ProjectModel.java | 6 +++--- .../segments/SegmentConditionModel.java | 4 ++-- .../segments/SegmentModel.java | 6 +++--- .../segments/SegmentRuleModel.java | 2 +- 12 files changed, 38 insertions(+), 38 deletions(-) rename src/main/java/com/flagsmith/{flagengine => models}/environments/EnvironmentModel.java (70%) rename src/main/java/com/flagsmith/{flagengine => models}/features/FeatureModel.java (73%) rename src/main/java/com/flagsmith/{flagengine => models}/features/FeatureSegmentModel.java (51%) rename src/main/java/com/flagsmith/{flagengine => models}/features/FeatureStateModel.java (94%) rename src/main/java/com/flagsmith/{flagengine => models}/features/MultivariateFeatureOptionModel.java (78%) rename src/main/java/com/flagsmith/{flagengine => models}/features/MultivariateFeatureStateValueModel.java (92%) rename src/main/java/com/flagsmith/{flagengine => models}/identities/IdentityModel.java (85%) rename src/main/java/com/flagsmith/{flagengine => models}/projects/ProjectModel.java (64%) rename src/main/java/com/flagsmith/{flagengine => models}/segments/SegmentConditionModel.java (87%) rename src/main/java/com/flagsmith/{flagengine => models}/segments/SegmentModel.java (77%) rename src/main/java/com/flagsmith/{flagengine => models}/segments/SegmentRuleModel.java (82%) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index dbfad027..fb71820f 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -13,18 +13,18 @@ import com.flagsmith.flagengine.SegmentRule; import com.flagsmith.flagengine.Segments; import com.flagsmith.flagengine.Traits; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureModel; -import com.flagsmith.flagengine.features.FeatureSegmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.features.MultivariateFeatureStateValueModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.projects.ProjectModel; -import com.flagsmith.flagengine.segments.SegmentConditionModel; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.flagengine.segments.SegmentRuleModel; import com.flagsmith.flagengine.segments.constants.SegmentConditions; 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; diff --git a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java b/src/main/java/com/flagsmith/models/environments/EnvironmentModel.java similarity index 70% rename from src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java rename to src/main/java/com/flagsmith/models/environments/EnvironmentModel.java index 912dec42..bfac9f63 100644 --- a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java +++ b/src/main/java/com/flagsmith/models/environments/EnvironmentModel.java @@ -1,9 +1,9 @@ -package com.flagsmith.flagengine.environments; +package com.flagsmith.models.environments; import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.projects.ProjectModel; +import com.flagsmith.models.features.FeatureStateModel; +import com.flagsmith.models.identities.IdentityModel; +import com.flagsmith.models.projects.ProjectModel; import com.flagsmith.utils.models.BaseModel; import java.util.List; import lombok.Data; @@ -25,4 +25,4 @@ public class EnvironmentModel extends BaseModel { @JsonProperty("identity_overrides") private List identityOverrides; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java b/src/main/java/com/flagsmith/models/features/FeatureModel.java similarity index 73% rename from src/main/java/com/flagsmith/flagengine/features/FeatureModel.java rename to src/main/java/com/flagsmith/models/features/FeatureModel.java index 7285d105..761371c3 100644 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java +++ b/src/main/java/com/flagsmith/models/features/FeatureModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.features; +package com.flagsmith.models.features; import lombok.Data; @@ -7,4 +7,4 @@ public class FeatureModel { private Integer id; private String name; private String type; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java b/src/main/java/com/flagsmith/models/features/FeatureSegmentModel.java similarity index 51% rename from src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java rename to src/main/java/com/flagsmith/models/features/FeatureSegmentModel.java index f7a55d88..619b639f 100644 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java +++ b/src/main/java/com/flagsmith/models/features/FeatureSegmentModel.java @@ -1,9 +1,9 @@ -package com.flagsmith.flagengine.features; +package com.flagsmith.models.features; -import com.flagsmith.flagengine.utils.models.BaseModel; +import com.flagsmith.utils.models.BaseModel; import lombok.Data; @Data public class FeatureSegmentModel extends BaseModel { private Integer priority; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java b/src/main/java/com/flagsmith/models/features/FeatureStateModel.java similarity index 94% rename from src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java rename to src/main/java/com/flagsmith/models/features/FeatureStateModel.java index 1057e937..ccaef00f 100644 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java +++ b/src/main/java/com/flagsmith/models/features/FeatureStateModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.features; +package com.flagsmith.models.features; import com.fasterxml.jackson.annotation.JsonProperty; import com.flagsmith.utils.models.BaseModel; @@ -20,4 +20,4 @@ public class FeatureStateModel extends BaseModel { private Object value; @JsonProperty("feature_segment") private FeatureSegmentModel featureSegment; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java b/src/main/java/com/flagsmith/models/features/MultivariateFeatureOptionModel.java similarity index 78% rename from src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java rename to src/main/java/com/flagsmith/models/features/MultivariateFeatureOptionModel.java index e5b95317..115618a3 100644 --- a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java +++ b/src/main/java/com/flagsmith/models/features/MultivariateFeatureOptionModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.features; +package com.flagsmith.models.features; import com.flagsmith.utils.models.BaseModel; import lombok.Data; @@ -6,4 +6,4 @@ @Data public class MultivariateFeatureOptionModel extends BaseModel { private String value; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java b/src/main/java/com/flagsmith/models/features/MultivariateFeatureStateValueModel.java similarity index 92% rename from src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java rename to src/main/java/com/flagsmith/models/features/MultivariateFeatureStateValueModel.java index 853129c9..e67749ec 100644 --- a/src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java +++ b/src/main/java/com/flagsmith/models/features/MultivariateFeatureStateValueModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.features; +package com.flagsmith.models.features; import com.fasterxml.jackson.annotation.JsonProperty; import com.flagsmith.utils.models.BaseModel; @@ -14,4 +14,4 @@ public class MultivariateFeatureStateValueModel extends BaseModel { private Integer id; @JsonProperty("mv_fs_value_uuid") private String mvFsValueUuid = UUID.randomUUID().toString(); -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java b/src/main/java/com/flagsmith/models/identities/IdentityModel.java similarity index 85% rename from src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java rename to src/main/java/com/flagsmith/models/identities/IdentityModel.java index 366b4685..7d56f912 100644 --- a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java +++ b/src/main/java/com/flagsmith/models/identities/IdentityModel.java @@ -1,7 +1,7 @@ -package com.flagsmith.flagengine.identities; +package com.flagsmith.models.identities; import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.utils.models.BaseModel; import java.sql.Date; import java.util.ArrayList; @@ -20,4 +20,4 @@ public class IdentityModel extends BaseModel { private String identityUuid = UUID.randomUUID().toString(); @JsonProperty("identity_features") private List identityFeatures = new ArrayList<>(); -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java b/src/main/java/com/flagsmith/models/projects/ProjectModel.java similarity index 64% rename from src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java rename to src/main/java/com/flagsmith/models/projects/ProjectModel.java index 6924fa4d..8e77f5b1 100644 --- a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java +++ b/src/main/java/com/flagsmith/models/projects/ProjectModel.java @@ -1,6 +1,6 @@ -package com.flagsmith.flagengine.projects; +package com.flagsmith.models.projects; -import com.flagsmith.flagengine.segments.SegmentModel; +import com.flagsmith.models.segments.SegmentModel; import com.flagsmith.utils.models.BaseModel; import java.util.List; import lombok.Data; @@ -8,4 +8,4 @@ @Data public class ProjectModel extends BaseModel { private List segments; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java b/src/main/java/com/flagsmith/models/segments/SegmentConditionModel.java similarity index 87% rename from src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java rename to src/main/java/com/flagsmith/models/segments/SegmentConditionModel.java index aa7ec45f..e4fb910a 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java +++ b/src/main/java/com/flagsmith/models/segments/SegmentConditionModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.segments; +package com.flagsmith.models.segments; import com.fasterxml.jackson.annotation.JsonProperty; import com.flagsmith.flagengine.segments.constants.SegmentConditions; @@ -10,4 +10,4 @@ public class SegmentConditionModel { private String value; @JsonProperty("property_") private String property; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java b/src/main/java/com/flagsmith/models/segments/SegmentModel.java similarity index 77% rename from src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java rename to src/main/java/com/flagsmith/models/segments/SegmentModel.java index 3ec15fef..6bfae3bc 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentModel.java +++ b/src/main/java/com/flagsmith/models/segments/SegmentModel.java @@ -1,7 +1,7 @@ -package com.flagsmith.flagengine.segments; +package com.flagsmith.models.segments; import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.utils.models.BaseModel; import java.util.List; import lombok.Data; @@ -13,4 +13,4 @@ public class SegmentModel extends BaseModel { private List rules; @JsonProperty("feature_states") private List featureStates; -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java b/src/main/java/com/flagsmith/models/segments/SegmentRuleModel.java similarity index 82% rename from src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java rename to src/main/java/com/flagsmith/models/segments/SegmentRuleModel.java index 08977e29..5e904f2c 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java +++ b/src/main/java/com/flagsmith/models/segments/SegmentRuleModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.segments; +package com.flagsmith.models.segments; import java.util.List; import lombok.Data; From 27b4b8e6cb6029a02a95a6131ad28ff5e81ceb00 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 15 Oct 2025 18:39:41 +0100 Subject: [PATCH 47/62] restore IOfflineHandler --- src/main/java/com/flagsmith/FlagsmithClient.java | 3 ++- .../com/flagsmith/interfaces/IOfflineHandler.java | 4 ++-- .../com/flagsmith/offline/LocalFileHandler.java | 13 +++++-------- .../java/com/flagsmith/DummyOfflineHandler.java | 6 +++--- .../java/com/flagsmith/FlagsmithTestHelper.java | 9 +++++++++ .../com/flagsmith/offline/LocalFileHandlerTest.java | 2 +- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 29c6e17e..ef1e1bc4 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -549,7 +549,8 @@ public FlagsmithClient build() { throw new FlagsmithRuntimeError( "Cannot use both default flag handler and offline handler."); } - client.evaluationContext = configuration.getOfflineHandler().getEvaluationContext(); + client.evaluationContext = EngineMappers.mapEnvironmentToContext( + configuration.getOfflineHandler().getEnvironment()); } return this.client; diff --git a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java index 481d6c35..ab574089 100644 --- a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java +++ b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java @@ -1,7 +1,7 @@ package com.flagsmith.interfaces; -import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.models.environments.EnvironmentModel; public interface IOfflineHandler { - EvaluationContext getEvaluationContext(); + EnvironmentModel getEnvironment(); } diff --git a/src/main/java/com/flagsmith/offline/LocalFileHandler.java b/src/main/java/com/flagsmith/offline/LocalFileHandler.java index 2cd91fdd..d12c8708 100644 --- a/src/main/java/com/flagsmith/offline/LocalFileHandler.java +++ b/src/main/java/com/flagsmith/offline/LocalFileHandler.java @@ -1,17 +1,15 @@ package com.flagsmith.offline; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.MapperFactory; import com.flagsmith.exceptions.FlagsmithClientError; -import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.interfaces.IOfflineHandler; -import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.models.environments.EnvironmentModel; import java.io.File; import java.io.IOException; public class LocalFileHandler implements IOfflineHandler { - private EvaluationContext evaluationContext; + private EnvironmentModel environmentModel; private ObjectMapper objectMapper = MapperFactory.getMapper(); /** @@ -22,14 +20,13 @@ public class LocalFileHandler implements IOfflineHandler { public LocalFileHandler(String filePath) throws FlagsmithClientError { File file = new File(filePath); try { - JsonNode environmentDocument = objectMapper.readValue(file, JsonNode.class); - this.evaluationContext = EngineMappers.mapEnvironmentDocumentToContext(environmentDocument); + this.environmentModel = objectMapper.readValue(file, EnvironmentModel.class); } catch (IOException e) { throw new FlagsmithClientError("Unable to read environment from file " + filePath); } } - public EvaluationContext getEvaluationContext() { - return evaluationContext; + public EnvironmentModel getEnvironment() { + return environmentModel; } } diff --git a/src/test/java/com/flagsmith/DummyOfflineHandler.java b/src/test/java/com/flagsmith/DummyOfflineHandler.java index 454256a7..b7bc783c 100644 --- a/src/test/java/com/flagsmith/DummyOfflineHandler.java +++ b/src/test/java/com/flagsmith/DummyOfflineHandler.java @@ -1,10 +1,10 @@ package com.flagsmith; -import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.interfaces.IOfflineHandler; +import com.flagsmith.models.environments.EnvironmentModel; public class DummyOfflineHandler implements IOfflineHandler { - public EvaluationContext getEvaluationContext() { - return FlagsmithTestHelper.evaluationContext(); + public EnvironmentModel getEnvironment() { + return FlagsmithTestHelper.environmentModel(); } } diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index 7a904f43..d60eae43 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -13,6 +13,7 @@ import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flag; import com.flagsmith.models.TraitModel; +import com.flagsmith.models.environments.EnvironmentModel; import com.google.common.collect.ImmutableMap; import io.restassured.RestAssured; import io.restassured.http.Header; @@ -348,6 +349,14 @@ public static String environmentString() { "}"; } + public static EnvironmentModel environmentModel() { + try { + return MapperFactory.getMapper().readValue(environmentString(), EnvironmentModel.class); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse environment JSON", e); + } + } + public static EvaluationContext evaluationContext() { try { return EngineMappers.mapEnvironmentDocumentToContext(MapperFactory.getMapper().readTree(environmentString())); diff --git a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java index fdf68183..74efe40d 100644 --- a/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java +++ b/src/test/java/com/flagsmith/offline/LocalFileHandlerTest.java @@ -24,7 +24,7 @@ public void testLocalFileHandler() throws FlagsmithClientError, IOException { LocalFileHandler handler = new LocalFileHandler(file.getAbsolutePath()); // Then - assertEquals(FlagsmithTestHelper.evaluationContext(), handler.getEvaluationContext()); + assertEquals(FlagsmithTestHelper.environmentModel(), handler.getEnvironment()); file.delete(); } From 000e96cbc90922c096426786be9445a1fb02bf0c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 16 Oct 2025 01:04:57 +0100 Subject: [PATCH 48/62] cleanup to improve diff --- pom.xml | 2 +- .../flagsmith/offline/LocalFileHandler.java | 2 +- .../com/flagsmith/DummyOfflineHandler.java | 6 +- .../com/flagsmith/FlagsmithClientTest.java | 1753 ++++++++--------- .../com/flagsmith/flagengine/EngineTest.java | 2 +- .../unit/segments/SegmentEvaluatorTest.java | 12 +- 6 files changed, 869 insertions(+), 908 deletions(-) diff --git a/pom.xml b/pom.xml index 2f4851e4..bafddad3 100644 --- a/pom.xml +++ b/pom.xml @@ -351,4 +351,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/flagsmith/offline/LocalFileHandler.java b/src/main/java/com/flagsmith/offline/LocalFileHandler.java index d12c8708..c604ed3c 100644 --- a/src/main/java/com/flagsmith/offline/LocalFileHandler.java +++ b/src/main/java/com/flagsmith/offline/LocalFileHandler.java @@ -20,7 +20,7 @@ public class LocalFileHandler implements IOfflineHandler { public LocalFileHandler(String filePath) throws FlagsmithClientError { File file = new File(filePath); try { - this.environmentModel = objectMapper.readValue(file, EnvironmentModel.class); + environmentModel = objectMapper.readValue(file, EnvironmentModel.class); } catch (IOException e) { throw new FlagsmithClientError("Unable to read environment from file " + filePath); } diff --git a/src/test/java/com/flagsmith/DummyOfflineHandler.java b/src/test/java/com/flagsmith/DummyOfflineHandler.java index b7bc783c..ff3eea8e 100644 --- a/src/test/java/com/flagsmith/DummyOfflineHandler.java +++ b/src/test/java/com/flagsmith/DummyOfflineHandler.java @@ -4,7 +4,7 @@ import com.flagsmith.models.environments.EnvironmentModel; public class DummyOfflineHandler implements IOfflineHandler { - public EnvironmentModel getEnvironment() { - return FlagsmithTestHelper.environmentModel(); - } + public EnvironmentModel getEnvironment() { + return FlagsmithTestHelper.environmentModel(); + } } diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 89d40ac6..c4ee9f69 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -7,10 +7,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -20,10 +20,10 @@ import com.flagsmith.exceptions.FlagsmithClientError; import com.flagsmith.exceptions.FlagsmithRuntimeError; import com.flagsmith.flagengine.EvaluationContext; -import com.flagsmith.flagengine.SegmentContext; import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.models.BaseFlag; import com.flagsmith.models.DefaultFlag; +import com.flagsmith.models.environments.EnvironmentModel; import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.SdkTraitModel; @@ -62,911 +62,872 @@ */ public class FlagsmithClientTest { - private static String DEFAULT_FLAG_VALUE = "foobar"; - private static boolean DEFAULT_FLAG_STATE = true; - - private static BaseFlag defaultHandler(String featureName) { - DefaultFlag defaultFlag = new DefaultFlag(); - defaultFlag.setEnabled(DEFAULT_FLAG_STATE); - defaultFlag.setValue(DEFAULT_FLAG_VALUE); - defaultFlag.setFeatureName(featureName); - return defaultFlag; - } - - @Test - public void testClient_When_Cache_Disabled_Return_Null() { - FlagsmithClient client = FlagsmithClient.newBuilder() - .setApiKey("api-key") - .build(); - - FlagsmithCache cache = client.getCache(); - - assertNull(cache); - } - - @Test - public void testClient_validateObjectCreation() throws InterruptedException { - PollingManager manager = mock(PollingManager.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withPollingManager(manager) - .withConfiguration( - FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) - .setApiKey("ser.abcdefg") - .build(); - - Thread.sleep(10); - verify(manager, times(1)).startPolling(); - } - - @Test - public void testLocalEvaluationRequiresServerKey() throws InterruptedException { - assertThrows(RuntimeException.class, () -> FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) - .setApiKey("not-a-server-key") - .build()); - } - - @Test - public void testClient_errorEnvironmentApi() { - Logger logger = mock(Logger.class); - - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .enableLogging(logger) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - client.updateEnvironment(); - - // Verify that an error was written to the log by mocking the logger and - // checking that a call was made - // with the expected log message. Note that the logger will also have other - // invocations so we need to - // iterate over them to check that the one we expect has been made. - boolean found = false; - String expectedMsg = "Unable to update environment from API. No environment configured - using defaultHandler if configured."; - for (Invocation invocation : Mockito.mockingDetails(logger).getInvocations().stream() - .collect(Collectors.toList())) { - if (invocation.getArgument(0).toString().contains(expectedMsg)) { - found = true; - } + private static String DEFAULT_FLAG_VALUE = "foobar"; + private static boolean DEFAULT_FLAG_STATE = true; + + private static BaseFlag defaultHandler(String featureName) { + DefaultFlag defaultFlag = new DefaultFlag(); + defaultFlag.setEnabled(DEFAULT_FLAG_STATE); + defaultFlag.setValue(DEFAULT_FLAG_VALUE); + defaultFlag.setFeatureName(featureName); + return defaultFlag; + } + + @Test + public void testClient_When_Cache_Disabled_Return_Null() { + FlagsmithClient client = FlagsmithClient.newBuilder() + .setApiKey("api-key") + .build(); + + FlagsmithCache cache = client.getCache(); + + assertNull(cache); + } + + @Test + public void testClient_validateObjectCreation() throws InterruptedException { + PollingManager manager = mock(PollingManager.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withPollingManager(manager) + .withConfiguration( + FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) + .setApiKey("ser.abcdefg") + .build(); + + Thread.sleep(10); + verify(manager, times(1)).startPolling(); + } + + @Test + public void testLocalEvaluationRequiresServerKey() throws InterruptedException { + assertThrows(RuntimeException.class, () -> FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder().withLocalEvaluation(Boolean.TRUE).build()) + .setApiKey("not-a-server-key") + .build()); + } + + @Test + public void testClient_errorEnvironmentApi() { + Logger logger = mock(Logger.class); + + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .enableLogging(logger) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + client.updateEnvironment(); + + // Verify that an error was written to the log by mocking the logger and checking that a call was made + // with the expected log message. Note that the logger will also have other invocations so we need to + // iterate over them to check that the one we expect has been made. + boolean found = false; + String expectedMsg = "Unable to update environment from API. No environment configured - using defaultHandler if configured."; + for (Invocation invocation : Mockito.mockingDetails(logger).getInvocations().stream().collect(Collectors.toList())) { + if (invocation.getArgument(0).toString().contains(expectedMsg)) { + found = true; + } + } + assertTrue(found); + } + + @Test + public void testClient_validateEnvironment() + throws JsonProcessingException { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + FlagsmithTestHelper.environmentString(), + MEDIATYPE_JSON); + + client.updateEnvironment(); + assertNotNull(client.getEvaluationContext()); + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testClient_flagsApiException() + throws FlagsmithApiError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, () -> client.getEnvironmentFlags()); + } + + @Test + public void testClient_flagsApiEmpty() + throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + "[]", + MEDIATYPE_JSON); + + assertNotNull(client); + List flags = client.getEnvironmentFlags().getAllFlags(); + assertTrue(flags.isEmpty()); + } + + @Test + public void testClient_flagsApi() + throws JsonProcessingException, FlagsmithClientError { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + List featureStateModel = FlagsmithTestHelper.getFlags(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + MapperFactory.getMapper().writeValueAsString(featureStateModel), + MEDIATYPE_JSON); + + List flags = client.getEnvironmentFlags().getAllFlags(); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + @Test + public void testClient_identityFlagsApiNoTraitsException() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .post(baseUrl + "/identities/") + .respond( + 500, + ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, () -> client.getIdentityFlags(identifier)); + } + + @Test + public void testClient_identityFlagsApiNoTraits() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + MockInterceptor interceptor = new MockInterceptor(); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + String json = FlagsmithTestHelper.getIdentitiesFlags(); + + interceptor.addRule() + .post(baseUrl + "/identities/") + .respond( + json, + MEDIATYPE_JSON); + + List flags = client.getIdentityFlags(identifier).getAllFlags(); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + private static Stream dataProviderForIdentityFlagsApiWithTraitsTest() { + return Stream.of( + Arguments.of( + "identifier", + false, + new HashMap() { + { + put("some_trait", "some_value"); + put("transient_trait", new TraitConfig("transient_value", true)); } - assertTrue(found); - } - - @Test - public void testClient_validateEnvironment() - throws JsonProcessingException { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - FlagsmithTestHelper.environmentString(), - MEDIATYPE_JSON); - - client.updateEnvironment(); - assertNotNull(client.getEvaluationContext()); - assertEquals(client.getEvaluationContext(), evaluationContext); - } - - @Test - public void testClient_flagsApiException() - throws FlagsmithApiError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, () -> client.getEnvironmentFlags()); - } - - @Test - public void testClient_flagsApiEmpty() - throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - "[]", - MEDIATYPE_JSON); - - assertNotNull(client); - List flags = client.getEnvironmentFlags().getAllFlags(); - assertTrue(flags.isEmpty()); - } - - @Test - public void testClient_flagsApi() - throws JsonProcessingException, FlagsmithClientError { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - List featureStateModel = FlagsmithTestHelper.getFlags(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - MapperFactory.getMapper().writeValueAsString(featureStateModel), - MEDIATYPE_JSON); - - List flags = client.getEnvironmentFlags().getAllFlags(); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - @Test - public void testClient_identityFlagsApiNoTraitsException() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .post(baseUrl + "/identities/") - .respond( - 500, - ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, () -> client.getIdentityFlags(identifier)); - } - - @Test - public void testClient_identityFlagsApiNoTraits() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - MockInterceptor interceptor = new MockInterceptor(); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - String json = FlagsmithTestHelper.getIdentitiesFlags(); - - interceptor.addRule() - .post(baseUrl + "/identities/") - .respond( - json, - MEDIATYPE_JSON); - - List flags = client.getIdentityFlags(identifier).getAllFlags(); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - private static Stream dataProviderForIdentityFlagsApiWithTraitsTest() { - return Stream.of( - Arguments.of( - "identifier", - false, - new HashMap() { - { - put("some_trait", "some_value"); - put("transient_trait", new TraitConfig( - "transient_value", true)); - } - }, FlagsmithTestHelper.getIdentityRequest("identifier", - new ArrayList() { - { - add( - SdkTraitModel.builder() - .traitKey("some_trait") - .traitValue("some_value") - .build()); - add( - SdkTraitModel.builder() - .traitKey("transient_trait") - .traitValue("transient_value") - .isTransient(true) - .build()); - } - })), - Arguments.of( - "transient-identifier", - true, - new HashMap() { - { - put("some_trait", "some_value"); - } - }, FlagsmithTestHelper.getIdentityRequest("transient-identifier", - new ArrayList() { - { - add( - TraitModel.builder() - .traitKey("some_trait") - .traitValue("some_value") - .build()); - } - }, true))); - } - - @ParameterizedTest - @MethodSource("dataProviderForIdentityFlagsApiWithTraitsTest") - public void testClient_identityFlagsApiWithTraits( - String identifier, boolean isTransient, Map traits, JsonNode expectedRequest) - throws FlagsmithClientError, IOException { - String baseUrl = "http://bad-url"; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - // mocking the requestor - ((FlagsmithApiWrapper) client.getFlagsmithSdk()).setRequestor(requestProcessor); - String json = FlagsmithTestHelper.getIdentitiesFlags(); - TypeReference tr = new TypeReference() { - }; - - when(requestProcessor.executeAsync(any(), any(), any())) - .thenReturn( - FlagsmithTestHelper.futurableReturn( - MapperFactory.getMapper().readValue(json, tr))); - - List flags = client.getIdentityFlags(identifier, traits, isTransient).getAllFlags(); - - ArgumentCaptor argument = ArgumentCaptor.forClass(Request.class); - verify(requestProcessor, times(1)).executeAsync(argument.capture(), any(), any()); - - Buffer buffer = new Buffer(); - argument.getValue().body().writeTo(buffer); - - assertEquals(expectedRequest.toString(), buffer.readUtf8()); - assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); - assertEquals(flags.get(0).getValue(), "some-value"); - assertEquals(flags.get(0).getFeatureName(), "some_feature"); - } - - @Test - public void testClient_identityFlagsApiWithTraitsWithLocalEnvironment() { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("some_trait", "some_value"); - } - }; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/").anyTimes() - .respond(500, ResponseBody.create("error", MEDIATYPE_JSON)); - - assertThrows(FlagsmithApiError.class, - () -> client.getEnvironmentFlags()); - } - - @Test - public void testClient_defaultFlagWithNoEnvironment() throws FlagsmithClientError { - String baseUrl = "http://bad-url"; - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("some_trait", "some_value"); - } - }; - MockInterceptor interceptor = new MockInterceptor(); - RequestProcessor requestProcessor = mock(RequestProcessor.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .build()) - .setApiKey("api-key") - .setDefaultFlagValueFunction((name) -> { - DefaultFlag flag = new DefaultFlag(); - flag.setValue("some-value"); - flag.setEnabled(true); - - return flag; - }) - .build(); - - interceptor.addRule() - .get(baseUrl + "/flags/") - .respond( - "[]", - MEDIATYPE_JSON); - - Flags flags = client.getEnvironmentFlags(); - - DefaultFlag flag = (DefaultFlag) flags.getFlag("some_feature"); - assertEquals(flag.getIsDefault(), Boolean.TRUE); - assertEquals(flag.getEnabled(), Boolean.TRUE); - assertEquals(flag.getValue(), "some-value"); - } - - @Test - public void testClient_When_Cache_Enabled_Return_Cache_Obj() { - FlagsmithClient client = FlagsmithClient.newBuilder() - .setApiKey("api-key") - .withCache(FlagsmithCacheConfig - .newBuilder() - .enableEnvLevelCaching("newkey-random-name") - .maxSize(2) - .build()) - .build(); - - FlagsmithCache cache = client.getCache(); - - assertNotNull(cache); - } - - @Test - public void testGetIdentitySegmentsNoTraits() throws JsonProcessingException, - FlagsmithClientError { - String baseUrl = "http://bad-url"; - - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - FlagsmithTestHelper.environmentString(), - MEDIATYPE_JSON); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .withLocalEvaluation(true) - .build()) - .setApiKey("ser.abcdefg") - .build(); - - client.updateEnvironment(); - - String identifier = "identifier"; - List segments = client.getIdentitySegments(identifier); - - assertTrue(segments.isEmpty()); - } - - @Test - public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingException, - FlagsmithClientError { - String baseUrl = "http://bad-url"; - - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - FlagsmithTestHelper.environmentString(), - MEDIATYPE_JSON); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .withLocalEvaluation(true) - .build()) - .setApiKey("ser.abcdefg") - .build(); - - client.updateEnvironment(); - - String identifier = "identifier"; - Map traits = new HashMap() { - { - put("foo", "bar"); - } - }; - - List segments = client.getIdentitySegments(identifier, traits); - - assertEquals(segments.size(), 1); - assertEquals(segments.get(0).getName(), "Test segment"); - } - - @Test - public void testGetIdentitySegments__NonAPISourceInMetadata__ReturnsExpected() throws JsonProcessingException, - FlagsmithClientError { - String baseUrl = "http://bad-url"; - - MockInterceptor interceptor = new MockInterceptor(); - interceptor.addRule() - .get(baseUrl + "/environment-document/") - .anyTimes() - .respond( - FlagsmithTestHelper.environmentString(), - MEDIATYPE_JSON); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withConfiguration( - FlagsmithConfig.newBuilder() - .baseUri(baseUrl) - .addHttpInterceptor(interceptor) - .withLocalEvaluation(true) - .build()) - .setApiKey("ser.abcdefg") - .build(); - - client.updateEnvironment(); - - String identifier = "overridden-identity"; - Map traits = new HashMap() { - { - put("foo", "bar"); - } - }; - - List segments = client.getIdentitySegments(identifier, traits); - - // no identity overrides segment present - assertEquals(segments.size(), 1); - assertEquals(segments.get(0).getName(), "Test segment"); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEvaluationContext()) - .thenReturn(evaluationContext) - .thenThrow(RuntimeException.class); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - // we call the update environment method twice (1st should be successful, 2nd - // will do nothing because of error) - client.updateEnvironment(); - client.updateEnvironment(); - - // Then - // No exception is thrown and the client environment remains what was first - // retrieved from the ApiWrapper - assertEquals(client.getEvaluationContext(), evaluationContext); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentExists() { - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEvaluationContext()) - .thenReturn(evaluationContext) - .thenReturn(null); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - // we call the update environment method twice - // (1st should be successful, 2nd will do nothing because of null return) - client.updateEnvironment(); - client.updateEnvironment(); - - // Then - // The client environment is not overwritten with null - assertEquals(client.getEvaluationContext(), evaluationContext); - } - - @Test - public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentNotExists() { - // Given - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.updateEnvironment(); - - // Then - // The environment remains null - assertEquals(client.getEvaluationContext(), null); - } - - @Test - public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() - throws FlagsmithClientError { - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - FlagsmithConfig config = FlagsmithConfig.newBuilder() - .withLocalEvaluation(true) - .build(); - - FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEvaluationContext()).thenReturn(evaluationContext); - when(mockApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.updateEnvironment(); - - // Then - // Identity overrides are correctly stored - assertEquals( - client.getIdentityFlags("overridden-identity") - .getFlag("some_feature").getValue(), - "overridden-value"); - } - - @Test - public void testClose_StopsPollingManager() { - // Given - PollingManager mockedPollingManager = mock(PollingManager.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withPollingManager(mockedPollingManager) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - verify(mockedPollingManager, times(1)).stopPolling(); - } - - @Test - public void testClose_ClosesFlagsmithSdk() { - // Given - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - verify(mockedApiWrapper, times(1)).close(); - } - - @Test - public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClientError { - // Specific test to ensure that results are consistent when making multiple - // calls to - // evaluate flags soon after the client is instantiated. - - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEvaluationContext()) - .thenReturn(evaluationContext) - .thenReturn(null); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - // When - // make 3 calls to get identity flags - List results = new ArrayList<>(); - for (int i = 0; i < 3; ++i) { - results.add(client.getIdentityFlags("some-identity")); + }, FlagsmithTestHelper.getIdentityRequest("identifier", new ArrayList() { + { + add( + SdkTraitModel.builder() + .traitKey("some_trait") + .traitValue("some_value") + .build() + ); + add( + SdkTraitModel.builder() + .traitKey("transient_trait") + .traitValue("transient_value") + .isTransient(true) + .build() + ); } - - // Then - // iterate over the results list and verify that the results are all the same - boolean expectedState = true; - String expectedValue = "some-value"; - - for (Flags flags : results) { - assertEquals(flags.isFeatureEnabled("some_feature"), expectedState); - assertEquals(flags.getFeatureValue("some_feature"), expectedValue); + })), + Arguments.of( + "transient-identifier", + true, + new HashMap() { + { + put("some_trait", "some_value"); } - } - - @Test - public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEvaluationContext()) - .thenReturn(evaluationContext) - .thenReturn(null); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - Flags flagsWithoutOverride = client.getIdentityFlags("test"); - - // When - Flags flagsWithOverride = client.getIdentityFlags("overridden-identity"); - - // Then - assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value"); - assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value"); - } - - @Test - public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() - throws FlagsmithClientError { - // Given - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) - .build(); - - // When - Flags environmentFlags = client.getEnvironmentFlags(); - - // Then - assertEquals(environmentFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); - assertEquals(environmentFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); - } - - @Test - public void testGetIdentityFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() - throws FlagsmithClientError { - // Given - FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) - .build(); - - // When - Flags identityFlags = client.getIdentityFlags("some-identity"); - - // Then - assertEquals(identityFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); - assertEquals(identityFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); - } - - @Test - public void testClose() throws FlagsmithApiError, InterruptedException { - // Given - int pollingIntervalSeconds = 1; - - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withLocalEvaluation(true) - .withEnvironmentRefreshIntervalSeconds(pollingIntervalSeconds) - .build(); - - FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEvaluationContext()).thenReturn(FlagsmithTestHelper.evaluationContext()); - when(mockedApiWrapper.getConfig()).thenReturn(config); - - FlagsmithClient client = FlagsmithClient.newBuilder() - .withFlagsmithApiWrapper(mockedApiWrapper) - .withConfiguration(config) - .setApiKey("ser.dummy-key") - .build(); - - // When - client.close(); - - // Then - // Since the thread will only stop once it reads the interrupt signal correctly - // on its next polling interval, we need to wait for the polling interval - // to complete before checking the thread has been killed correctly. - Thread.sleep((pollingIntervalSeconds * 1000) + 100); - assertFalse(client.getPollingManager().getIsThreadAlive()); - } - - @Test - public void testOfflineMode() throws FlagsmithClientError { - // Given - EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineMode(true) - .withOfflineHandler(new DummyOfflineHandler()) - .build(); - - // When - FlagsmithClient client = FlagsmithClient.newBuilder().withConfiguration(config).build(); - - // Then - assertEquals(evaluationContext, client.getEvaluationContext()); - - Flags environmentFlags = client.getEnvironmentFlags(); - assertTrue(environmentFlags.isFeatureEnabled("some_feature")); - - Flags identityFlags = client.getIdentityFlags("my-identity"); - assertTrue(identityFlags.isFeatureEnabled("some_feature")); - } - - @Test - public void testCannotUserOfflineModeWithoutOfflineHandler() throws FlagsmithRuntimeError { - FlagsmithConfig config = FlagsmithConfig.newBuilder().withOfflineMode(true).build(); - - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); - - assertEquals("Offline handler must be provided to use offline mode.", ex.getMessage()); - } - - @Test - public void testCannotUserOfflineHandlerWithLocalEvaluationMode() throws FlagsmithRuntimeError { - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineHandler(new DummyOfflineHandler()) + }, FlagsmithTestHelper.getIdentityRequest("transient-identifier", new ArrayList() { + { + add( + TraitModel.builder() + .traitKey("some_trait") + .traitValue("some_value") + .build() + ); + } + }, true)) + ); + } + + @ParameterizedTest + @MethodSource("dataProviderForIdentityFlagsApiWithTraitsTest") + public void testClient_identityFlagsApiWithTraits( + String identifier, boolean isTransient, Map traits, JsonNode expectedRequest) + throws FlagsmithClientError, IOException { + String baseUrl = "http://bad-url"; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + // mocking the requestor + ((FlagsmithApiWrapper) client.getFlagsmithSdk()).setRequestor(requestProcessor); + String json = FlagsmithTestHelper.getIdentitiesFlags(); + TypeReference tr = new TypeReference() { + }; + + when(requestProcessor.executeAsync(any(), any(), any())) + .thenReturn( + FlagsmithTestHelper.futurableReturn(MapperFactory.getMapper().readValue(json, tr))); + + List flags = client.getIdentityFlags(identifier, traits, isTransient).getAllFlags(); + + ArgumentCaptor argument = ArgumentCaptor.forClass(Request.class); + verify(requestProcessor, times(1)).executeAsync(argument.capture(), any(), any()); + + Buffer buffer = new Buffer(); + argument.getValue().body().writeTo(buffer); + + assertEquals(expectedRequest.toString(), buffer.readUtf8()); + assertEquals(flags.get(0).getEnabled(), Boolean.TRUE); + assertEquals(flags.get(0).getValue(), "some-value"); + assertEquals(flags.get(0).getFeatureName(), "some_feature"); + } + + @Test + public void testClient_identityFlagsApiWithTraitsWithLocalEnvironment() { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("some_trait", "some_value"); + } + }; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/").anyTimes() + .respond(500, ResponseBody.create("error", MEDIATYPE_JSON)); + + assertThrows(FlagsmithApiError.class, + () -> client.getEnvironmentFlags()); + } + + @Test + public void testClient_defaultFlagWithNoEnvironment() throws FlagsmithClientError { + String baseUrl = "http://bad-url"; + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("some_trait", "some_value"); + } + }; + MockInterceptor interceptor = new MockInterceptor(); + RequestProcessor requestProcessor = mock(RequestProcessor.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .build()) + .setApiKey("api-key") + .setDefaultFlagValueFunction((name) -> { + DefaultFlag flag = new DefaultFlag(); + flag.setValue("some-value"); + flag.setEnabled(true); + + return flag; + }) + .build(); + + interceptor.addRule() + .get(baseUrl + "/flags/") + .respond( + "[]", + MEDIATYPE_JSON); + + Flags flags = client.getEnvironmentFlags(); + + DefaultFlag flag = (DefaultFlag) flags.getFlag("some_feature"); + assertEquals(flag.getIsDefault(), Boolean.TRUE); + assertEquals(flag.getEnabled(), Boolean.TRUE); + assertEquals(flag.getValue(), "some-value"); + } + + @Test + public void testClient_When_Cache_Enabled_Return_Cache_Obj() { + FlagsmithClient client = FlagsmithClient.newBuilder() + .setApiKey("api-key") + .withCache(FlagsmithCacheConfig + .newBuilder() + .enableEnvLevelCaching("newkey-random-name") + .maxSize(2) + .build()) + .build(); + + FlagsmithCache cache = client.getCache(); + + assertNotNull(cache); + } + + @Test + public void testGetIdentitySegmentsNoTraits() throws JsonProcessingException, + FlagsmithClientError { + String baseUrl = "http://bad-url"; + + EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + MapperFactory.getMapper().writeValueAsString(environmentModel), + MEDIATYPE_JSON); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) .withLocalEvaluation(true) - .build(); + .build()) + .setApiKey("ser.abcdefg") + .build(); - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); + client.updateEnvironment(); - assertEquals("Local evaluation and offline handler cannot be used together.", ex.getMessage()); - } + String identifier = "identifier"; + List segments = client.getIdentitySegments(identifier); - @Test - public void testCannotUseDefaultHandlerAndOfflineHandler() throws FlagsmithClientError { - FlagsmithConfig config = FlagsmithConfig - .newBuilder() - .withOfflineHandler(new DummyOfflineHandler()) - .build(); + assertTrue(segments.isEmpty()); + } - FlagsmithClient.Builder clientBuilder = FlagsmithClient - .newBuilder() - .withConfiguration(config) - .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler); + @Test + public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingException, + FlagsmithClientError { + String baseUrl = "http://bad-url"; - FlagsmithRuntimeError ex = assertThrows( - FlagsmithRuntimeError.class, - () -> clientBuilder.build()); + EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); - assertEquals("Cannot use both default flag handler and offline handler.", ex.getMessage()); - } + MockInterceptor interceptor = new MockInterceptor(); + interceptor.addRule() + .get(baseUrl + "/environment-document/") + .anyTimes() + .respond( + MapperFactory.getMapper().writeValueAsString(environmentModel), + MEDIATYPE_JSON); - @Test - public void testFlagsmithUsesOfflineHandlerIfSetAndNoAPIResponse() throws FlagsmithClientError { - // Given - MockInterceptor interceptor = new MockInterceptor(); - String baseUrl = "http://bad-url"; - - FlagsmithConfig config = FlagsmithConfig - .newBuilder() + FlagsmithClient client = FlagsmithClient.newBuilder() + .withConfiguration( + FlagsmithConfig.newBuilder() .baseUri(baseUrl) .addHttpInterceptor(interceptor) - .withOfflineHandler(new DummyOfflineHandler()) - .build(); - FlagsmithClient client = FlagsmithClient - .newBuilder() - .withConfiguration(config) - .setApiKey("some-key") - .build(); - - interceptor.addRule().get(baseUrl + "/flags/").respond(500); - interceptor.addRule().post(baseUrl + "/identities/").respond(500); - - // When - Flags environmentFlags = client.getEnvironmentFlags(); - Flags identityFlags = client.getIdentityFlags("some-identity"); - - // Then - assertTrue(environmentFlags.isFeatureEnabled("some_feature")); - assertTrue(identityFlags.isFeatureEnabled("some_feature")); - } + .withLocalEvaluation(true) + .build()) + .setApiKey("ser.abcdefg") + .build(); + + client.updateEnvironment(); + + String identifier = "identifier"; + Map traits = new HashMap() { + { + put("foo", "bar"); + } + }; + + List segments = client.getIdentitySegments(identifier, traits); + + assertEquals(segments.size(), 1); + assertEquals(segments.get(0).getName(), "Test segment"); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenThrow(RuntimeException.class); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + // we call the update environment method twice (1st should be successful, 2nd + // will do nothing because of error) + client.updateEnvironment(); + client.updateEnvironment(); + + // Then + // No exception is thrown and the client environment remains what was first + // retrieved from the ApiWrapper + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentExists() { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + // we call the update environment method twice + // (1st should be successful, 2nd will do nothing because of null return) + client.updateEnvironment(); + client.updateEnvironment(); + + // Then + // The client environment is not overwritten with null + assertEquals(client.getEvaluationContext(), evaluationContext); + } + + @Test + public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentNotExists() { + // Given + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.updateEnvironment(); + + // Then + // The environment remains null + assertEquals(client.getEvaluationContext(), null); + } + + @Test + public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() + throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder() + .withLocalEvaluation(true) + .build(); + + FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockApiWrapper.getEvaluationContext()).thenReturn(evaluationContext); + when(mockApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.updateEnvironment(); + + // Then + // Identity overrides are correctly stored + assertEquals( + client.getIdentityFlags("overridden-identity") + .getFlag("some_feature").getValue(), + "overridden-value"); + } + + @Test + public void testClose_StopsPollingManager() { + // Given + PollingManager mockedPollingManager = mock(PollingManager.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withPollingManager(mockedPollingManager) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + verify(mockedPollingManager, times(1)).stopPolling(); + } + + @Test + public void testClose_ClosesFlagsmithSdk() { + // Given + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build()) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + verify(mockedApiWrapper, times(1)).close(); + } + + @Test + public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClientError { + // Specific test to ensure that results are consistent when making multiple + // calls to + // evaluate flags soon after the client is instantiated. + + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + // When + // make 3 calls to get identity flags + List results = new ArrayList<>(); + for (int i = 0; i < 3; ++i) { + results.add(client.getIdentityFlags("some-identity")); + } + + // Then + // iterate over the results list and verify that the results are all the same + boolean expectedState = true; + String expectedValue = "some-value"; + + for (Flags flags : results) { + assertEquals(flags.isFeatureEnabled("some_feature"), expectedState); + assertEquals(flags.getFeatureValue("some_feature"), expectedValue); + } + } + + @Test + public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) + .thenReturn(null); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + Flags flagsWithoutOverride = client.getIdentityFlags("test"); + + // When + Flags flagsWithOverride = client.getIdentityFlags("overridden-identity"); + + // Then + assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value"); + assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value"); + } + + @Test + public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() + throws FlagsmithClientError { + // Given + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) + .build(); + + // When + Flags environmentFlags = client.getEnvironmentFlags(); + + // Then + assertEquals(environmentFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); + assertEquals(environmentFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); + } + + @Test + public void testGetIdentityFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull() throws FlagsmithClientError { + // Given + FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler) + .build(); + + // When + Flags identityFlags = client.getIdentityFlags("some-identity"); + + // Then + assertEquals(identityFlags.getFeatureValue("foo"), DEFAULT_FLAG_VALUE); + assertEquals(identityFlags.isFeatureEnabled("foo"), DEFAULT_FLAG_STATE); + } + + @Test + public void testClose() throws FlagsmithApiError, InterruptedException { + // Given + int pollingIntervalSeconds = 1; + + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withLocalEvaluation(true) + .withEnvironmentRefreshIntervalSeconds(pollingIntervalSeconds) + .build(); + + FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); + when(mockedApiWrapper.getEvaluationContext()).thenReturn(FlagsmithTestHelper.evaluationContext()); + when(mockedApiWrapper.getConfig()).thenReturn(config); + + FlagsmithClient client = FlagsmithClient.newBuilder() + .withFlagsmithApiWrapper(mockedApiWrapper) + .withConfiguration(config) + .setApiKey("ser.dummy-key") + .build(); + + // When + client.close(); + + // Then + // Since the thread will only stop once it reads the interrupt signal correctly + // on its next polling interval, we need to wait for the polling interval + // to complete before checking the thread has been killed correctly. + Thread.sleep((pollingIntervalSeconds * 1000) + 100); + assertFalse(client.getPollingManager().getIsThreadAlive()); + } + + @Test + public void testOfflineMode() throws FlagsmithClientError { + // Given + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineMode(true) + .withOfflineHandler(new DummyOfflineHandler()) + .build(); + + // When + FlagsmithClient client = FlagsmithClient.newBuilder().withConfiguration(config).build(); + + // Then + assertEquals(evaluationContext, client.getEvaluationContext()); + + Flags environmentFlags = client.getEnvironmentFlags(); + assertTrue(environmentFlags.isFeatureEnabled("some_feature")); + + Flags identityFlags = client.getIdentityFlags("my-identity"); + assertTrue(identityFlags.isFeatureEnabled("some_feature")); + } + + @Test + public void testCannotUserOfflineModeWithoutOfflineHandler() throws FlagsmithRuntimeError { + FlagsmithConfig config = FlagsmithConfig.newBuilder().withOfflineMode(true).build(); + + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); + + assertEquals("Offline handler must be provided to use offline mode.", ex.getMessage()); + } + + @Test + public void testCannotUserOfflineHandlerWithLocalEvaluationMode() throws FlagsmithRuntimeError { + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineHandler(new DummyOfflineHandler()) + .withLocalEvaluation(true) + .build(); + + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> FlagsmithClient.newBuilder().withConfiguration(config).build()); + + assertEquals("Local evaluation and offline handler cannot be used together.", ex.getMessage()); + } + + @Test + public void testCannotUseDefaultHandlerAndOfflineHandler() throws FlagsmithClientError { + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .withOfflineHandler(new DummyOfflineHandler()) + .build(); + + FlagsmithClient.Builder clientBuilder = FlagsmithClient + .newBuilder() + .withConfiguration(config) + .setDefaultFlagValueFunction(FlagsmithClientTest::defaultHandler); + + FlagsmithRuntimeError ex = assertThrows( + FlagsmithRuntimeError.class, + () -> clientBuilder.build()); + + assertEquals("Cannot use both default flag handler and offline handler.", ex.getMessage()); + } + + @Test + public void testFlagsmithUsesOfflineHandlerIfSetAndNoAPIResponse() throws FlagsmithClientError { + // Given + MockInterceptor interceptor = new MockInterceptor(); + String baseUrl = "http://bad-url"; + + FlagsmithConfig config = FlagsmithConfig + .newBuilder() + .baseUri(baseUrl) + .addHttpInterceptor(interceptor) + .withOfflineHandler(new DummyOfflineHandler()) + .build(); + FlagsmithClient client = FlagsmithClient + .newBuilder() + .withConfiguration(config) + .setApiKey("some-key") + .build(); + + interceptor.addRule().get(baseUrl + "/flags/").respond(500); + interceptor.addRule().post(baseUrl + "/identities/").respond(500); + + // When + Flags environmentFlags = client.getEnvironmentFlags(); + Flags identityFlags = client.getIdentityFlags("some-identity"); + + // Then + assertTrue(environmentFlags.isFeatureEnabled("some_feature")); + assertTrue(identityFlags.isFeatureEnabled("some_feature")); + } } diff --git a/src/test/java/com/flagsmith/flagengine/EngineTest.java b/src/test/java/com/flagsmith/flagengine/EngineTest.java index e99a2fa4..547c6012 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -56,4 +56,4 @@ public void testEngine(EvaluationContext evaluationContext, EvaluationResult exp .ignoringAllOverriddenEquals() .isEqualTo(expectedResult); } -} \ No newline at end of file +} diff --git a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java index 9ce2890c..f7b23fd3 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -64,12 +64,12 @@ public void testContextInSegment(SegmentContext segment, List identi private static Stream traitExistenceChecks() { return Stream.of( - Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(), false), - Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(), true), - Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(Arrays.asList( - new TraitModel("foo", "bar"))), true), - Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(Arrays.asList( - new TraitModel("foo", "bar"))), false) + Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(), false), + Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(), true), + Arguments.of(SegmentConditions.IS_SET, "foo", new ArrayList<>(Arrays.asList( + new TraitModel("foo", "bar"))), true), + Arguments.of(SegmentConditions.IS_NOT_SET, "foo", new ArrayList<>(Arrays.asList( + new TraitModel("foo", "bar"))), false) ); } From 211c5bde15965d564cece608e2f534b61432c865 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 16 Oct 2025 17:18:03 +0100 Subject: [PATCH 49/62] support feature metadata, unset segments in context --- .gitmodules | 2 +- .../java/com/flagsmith/flagengine/Engine.java | 71 ++++++++++--------- .../com/flagsmith/flagengine/enginetestdata | 2 +- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/.gitmodules b/.gitmodules index 2e4b1ea7..4ff0518a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"] path = src/test/java/com/flagsmith/flagengine/enginetestdata url = git@github.com:Flagsmith/engine-test-data.git - tag = v2.4.0 \ No newline at end of file + tag = v2.5.0 \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index 8f61cc4a..f886ce2e 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -48,37 +48,41 @@ private static SegmentEvaluationResult evaluateSegments( List segments = new ArrayList<>(); HashMap> segmentFeatureContexts = new HashMap<>(); - for (SegmentContext segmentContext : context.getSegments().getAdditionalProperties().values()) { - if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { - segments.add(new SegmentResult().withKey(segmentContext.getKey()) - .withName(segmentContext.getName()) - .withMetadata(segmentContext.getMetadata())); - - List segmentOverrides = segmentContext.getOverrides(); - - if (segmentOverrides != null) { - for (FeatureContext featureContext : segmentOverrides) { - String featureKey = featureContext.getFeatureKey(); - - if (segmentFeatureContexts.containsKey(featureKey)) { - ImmutablePair existing = segmentFeatureContexts - .get(featureKey); - FeatureContext existingFeatureContext = existing.getRight(); - - Double existingPriority = existingFeatureContext.getPriority() == null - ? EngineConstants.WEAKEST_PRIORITY - : existingFeatureContext.getPriority(); - Double featurePriority = featureContext.getPriority() == null - ? EngineConstants.WEAKEST_PRIORITY - : featureContext.getPriority(); - - if (existingPriority < featurePriority) { - continue; + Segments contextSegments = context.getSegments(); + + if (contextSegments != null) { + for (SegmentContext segmentContext : contextSegments.getAdditionalProperties().values()) { + if (SegmentEvaluator.isContextInSegment(context, segmentContext)) { + segments.add(new SegmentResult().withKey(segmentContext.getKey()) + .withName(segmentContext.getName()) + .withMetadata(segmentContext.getMetadata())); + + List segmentOverrides = segmentContext.getOverrides(); + + if (segmentOverrides != null) { + for (FeatureContext featureContext : segmentOverrides) { + String featureKey = featureContext.getFeatureKey(); + + if (segmentFeatureContexts.containsKey(featureKey)) { + ImmutablePair existing = segmentFeatureContexts + .get(featureKey); + FeatureContext existingFeatureContext = existing.getRight(); + + Double existingPriority = existingFeatureContext.getPriority() == null + ? EngineConstants.WEAKEST_PRIORITY + : existingFeatureContext.getPriority(); + Double featurePriority = featureContext.getPriority() == null + ? EngineConstants.WEAKEST_PRIORITY + : featureContext.getPriority(); + + if (existingPriority < featurePriority) { + continue; + } } + segmentFeatureContexts.put(featureKey, + new ImmutablePair( + segmentContext.getName(), featureContext)); } - segmentFeatureContexts.put(featureKey, - new ImmutablePair( - segmentContext.getName(), featureContext)); } } } @@ -110,7 +114,8 @@ private static Flags evaluateFeatures( .withName(featureContext.getName()) .withValue(featureContext.getValue()) .withReason( - "TARGETING_MATCH; segment=" + segmentNameFeaturePair.getLeft())); + "TARGETING_MATCH; segment=" + segmentNameFeaturePair.getLeft()) + .withMetadata(featureContext.getMetadata())); } else { flags.setAdditionalProperty(featureContext.getName(), getFlagResultFromFeatureContext(featureContext, identityKey)); @@ -146,7 +151,8 @@ private static FlagResult getFlagResultFromFeatureContext( .withFeatureKey(featureContext.getFeatureKey()) .withName(featureContext.getName()) .withValue(variant.getValue()) - .withReason("SPLIT; weight=" + weight.intValue()); + .withReason("SPLIT; weight=" + weight.intValue()) + .withMetadata(featureContext.getMetadata()); } startPercentage = limit; } @@ -157,6 +163,7 @@ private static FlagResult getFlagResultFromFeatureContext( .withFeatureKey(featureContext.getFeatureKey()) .withName(featureContext.getName()) .withValue(featureContext.getValue()) - .withReason("DEFAULT"); + .withReason("DEFAULT") + .withMetadata(featureContext.getMetadata()); } } \ No newline at end of file diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index 6453b039..41c20214 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit 6453b0391344a4d677a97cc4a9d27a8b8e329787 +Subproject commit 41c202145e375c712600e318c439456de5b221d7 From 8389a901b3482bc5f74777e19eb0199964474eb5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 16 Oct 2025 17:20:10 +0100 Subject: [PATCH 50/62] remove extra FeatureStateModel --- .../com/flagsmith/FlagsmithApiWrapper.java | 2 +- .../flagsmith/models/FeatureStateModel.java | 63 ------------------- src/main/java/com/flagsmith/models/Flag.java | 1 + src/main/java/com/flagsmith/models/Flags.java | 1 + .../responses/FlagsAndTraitsResponse.java | 2 +- .../FlagsmithApiWrapperCachingTest.java | 2 +- .../flagsmith/FlagsmithApiWrapperTest.java | 5 +- .../com/flagsmith/FlagsmithClientTest.java | 2 +- .../com/flagsmith/FlagsmithTestHelper.java | 5 +- 9 files changed, 12 insertions(+), 71 deletions(-) delete mode 100644 src/main/java/com/flagsmith/models/FeatureStateModel.java diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index 0a05945d..4a955cbf 100644 --- a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java +++ b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java @@ -9,9 +9,9 @@ import com.flagsmith.interfaces.FlagsmithCache; import com.flagsmith.interfaces.FlagsmithSdk; import com.flagsmith.mappers.EngineMappers; -import com.flagsmith.models.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.TraitModel; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.responses.FlagsAndTraitsResponse; import com.flagsmith.threads.AnalyticsProcessor; import com.flagsmith.threads.RequestProcessor; diff --git a/src/main/java/com/flagsmith/models/FeatureStateModel.java b/src/main/java/com/flagsmith/models/FeatureStateModel.java deleted file mode 100644 index 6027e9c1..00000000 --- a/src/main/java/com/flagsmith/models/FeatureStateModel.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.flagsmith.models; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.utils.models.BaseModel; -import java.util.List; -import java.util.UUID; -import lombok.Data; - -@Data -public class FeatureStateModel extends BaseModel { - @Data - public class FeatureModel { - @JsonProperty("id") - private Integer id; - @JsonProperty("name") - private String name; - @JsonProperty("type") - private String type; - } - - @Data - public class MultivariateFeatureOptionModel { - @JsonProperty("id") - private Integer id; - @JsonProperty("value") - private String value; - } - - @Data - public class FeatureSegmentModel { - @JsonProperty("id") - private Integer id; - @JsonProperty("priority") - private Integer priority; - } - - @Data - public class MultivariateFeatureStateValueModel { - @JsonProperty("multivariate_feature_option") - private MultivariateFeatureOptionModel multivariateFeatureOption; - @JsonProperty("percentage_allocation") - private Float percentageAllocation; - @JsonProperty("id") - private Integer id; - - public Float getSortValue() { - return percentageAllocation != null ? percentageAllocation : 0f; - } - } - - 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; -} diff --git a/src/main/java/com/flagsmith/models/Flag.java b/src/main/java/com/flagsmith/models/Flag.java index 7589526f..990976b9 100644 --- a/src/main/java/com/flagsmith/models/Flag.java +++ b/src/main/java/com/flagsmith/models/Flag.java @@ -1,6 +1,7 @@ package com.flagsmith.models; import com.fasterxml.jackson.databind.JsonNode; +import com.flagsmith.models.features.FeatureStateModel; import lombok.Data; @Data diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index c69f8805..27474626 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -7,6 +7,7 @@ import com.flagsmith.flagengine.EvaluationResult; import com.flagsmith.flagengine.FlagResult; import com.flagsmith.interfaces.DefaultFlagHandler; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.threads.AnalyticsProcessor; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java b/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java index 4d3fcbe5..52e63b24 100644 --- a/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java +++ b/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java @@ -1,7 +1,7 @@ package com.flagsmith.responses; import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java index cba467c1..40320683 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java @@ -15,7 +15,7 @@ import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.config.Retry; import com.flagsmith.interfaces.FlagsmithCache; -import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.TraitModel; import com.flagsmith.responses.FlagsAndTraitsResponse; diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java index 2f9ef58a..7f6deb07 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java @@ -19,7 +19,8 @@ import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.config.Retry; import com.flagsmith.models.BaseFlag; -import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureModel; import com.flagsmith.models.Flag; import com.flagsmith.models.Flags; import com.flagsmith.models.TraitModel; @@ -194,7 +195,7 @@ public void testClose_ClosesAnalyticsProcessor() { private FeatureStateModel getNewFlag() { final FeatureStateModel flag = new FeatureStateModel(); - final FeatureStateModel.FeatureModel feature = flag.new FeatureModel(); + final FeatureModel feature = new FeatureModel(); feature.setName("my-test-flag"); feature.setId(123); flag.setFeature(feature); diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index c4ee9f69..3efe94bf 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -24,7 +24,7 @@ import com.flagsmith.models.BaseFlag; import com.flagsmith.models.DefaultFlag; import com.flagsmith.models.environments.EnvironmentModel; -import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.models.Flags; import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.Segment; diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index d60eae43..eaa2132f 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -10,7 +10,8 @@ import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.BaseFlag; -import com.flagsmith.models.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureModel; import com.flagsmith.models.Flag; import com.flagsmith.models.TraitModel; import com.flagsmith.models.environments.EnvironmentModel; @@ -245,7 +246,7 @@ public static BaseFlag flag( result.setEnabled(enabled); result.setValue(value); - final FeatureStateModel.FeatureModel feature = result.new FeatureModel(); + final FeatureModel feature = result.new FeatureModel(); feature.setName(name); feature.setType(type); From 69f00a466b743a26b3ef5683174eee360b3848ed Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 16 Oct 2025 18:37:20 +0100 Subject: [PATCH 51/62] support setting Flag.featureId via metadata --- .../com/flagsmith/mappers/EngineMappers.java | 45 ++++++++++++++++++- .../com/flagsmith/models/FeatureMetadata.java | 31 +++++++++++++ src/main/java/com/flagsmith/models/Flags.java | 24 ++++------ .../flagengine/models/FlagsTest.java | 44 ++++++++++++++++++ 4 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/flagsmith/models/FeatureMetadata.java create mode 100644 src/test/java/com/flagsmith/flagengine/models/FlagsTest.java diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index fb71820f..26087c31 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -7,6 +7,7 @@ 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; @@ -14,6 +15,8 @@ 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; @@ -37,6 +40,33 @@ *

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. * @@ -178,7 +208,8 @@ private static Map mapIdentityOverridesToSegments( .withName(feature.getName()) .withEnabled(featureState.getEnabled()) .withValue(featureState.getValue()) - .withPriority(EngineConstants.STRONGEST_PRIORITY); + .withPriority(EngineConstants.STRONGEST_PRIORITY) + .withMetadata(mapFeatureStateToFeatureMetadata(featureState)); overridesKey.add(featureContext); } @@ -332,7 +363,8 @@ private static FeatureContext mapFeatureStateToFeatureContext(FeatureStateModel .withFeatureKey(String.valueOf(featureState.getFeature().getId())) .withName(featureState.getFeature().getName()) .withEnabled(featureState.getEnabled()) - .withValue(featureState.getValue()); + .withValue(featureState.getValue()) + .withMetadata(mapFeatureStateToFeatureMetadata(featureState)); // Handle multivariate feature state values List variants = new ArrayList<>(); @@ -358,6 +390,15 @@ private static FeatureContext mapFeatureStateToFeatureContext(FeatureStateModel return featureContext; } + private static Map mapFeatureStateToFeatureMetadata( + FeatureStateModel featureState) { + FeatureMetadata metadata = new FeatureMetadata(); + metadata.setFlagsmithId(featureState.getFeature().getId()); + return MapperFactory.getMapper().convertValue(metadata, + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + } + /** * Maps a segment to segment context. * diff --git a/src/main/java/com/flagsmith/models/FeatureMetadata.java b/src/main/java/com/flagsmith/models/FeatureMetadata.java new file mode 100644 index 00000000..1dd2c82d --- /dev/null +++ b/src/main/java/com/flagsmith/models/FeatureMetadata.java @@ -0,0 +1,31 @@ +package com.flagsmith.models; + +/** + * FeatureMetadata + * + *

Additional metadata associated with a feature. + * + */ +public class FeatureMetadata { + private Integer flagsmithId; + + /* + * FlagsmithId + *

The internal Flagsmith ID for the feature. + * + * @return The flagsmithId + */ + public Integer getFlagsmithId() { + return flagsmithId; + } + + /* + * FlagsmithId + *

The internal Flagsmith ID for the segment. + * + * @param flagsmithId The flagsmithId + */ + public void setFlagsmithId(Integer flagsmithId) { + this.flagsmithId = flagsmithId; + } +} diff --git a/src/main/java/com/flagsmith/models/Flags.java b/src/main/java/com/flagsmith/models/Flags.java index 27474626..f09792bd 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -7,6 +7,7 @@ import com.flagsmith.flagengine.EvaluationResult; import com.flagsmith.flagengine.FlagResult; import com.flagsmith.interfaces.DefaultFlagHandler; +import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.threads.AnalyticsProcessor; import java.util.HashMap; @@ -127,21 +128,14 @@ public static Flags fromEvaluationResult( EvaluationResult evaluationResult, AnalyticsProcessor analyticsProcessor, DefaultFlagHandler defaultFlagHandler) { - Map flagMap = evaluationResult.getFlags().getAdditionalProperties() - .entrySet() - .stream() - .collect( - Collectors.toMap( - Entry::getKey, - entry -> { - FlagResult flagResult = entry.getValue(); - Flag flag = new Flag(); - flag.setFeatureName(flagResult.getName()); - flag.setValue(flagResult.getValue()); - flag.setEnabled(flagResult.getEnabled()); - return flag; - })); - + Map flagMap = new HashMap<>(); + evaluationResult.getFlags().getAdditionalProperties().forEach((featureName, flagResult) -> { + Flag flag = EngineMappers.mapFlagResultToFlag(flagResult); + if (flag != null) { + flagMap.put(featureName, flag); + } + }); + Flags flags = new Flags(); flags.setFlags(flagMap); flags.setAnalyticsProcessor(analyticsProcessor); diff --git a/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java b/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java new file mode 100644 index 00000000..bde11bfd --- /dev/null +++ b/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java @@ -0,0 +1,44 @@ +package com.flagsmith.flagengine.models; + +import com.flagsmith.exceptions.FlagsmithClientError; +import com.flagsmith.flagengine.EvaluationResult; +import com.flagsmith.models.Flags; +import com.flagsmith.models.Flag; +import com.flagsmith.flagengine.FlagResult; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +public class FlagsTest { + @Test + public void testFromEvaluationResult__metadata__expected() throws FlagsmithClientError { + com.flagsmith.flagengine.Flags flagResults = new com.flagsmith.flagengine.Flags() + .withAdditionalProperty("feature_1", new FlagResult() + .withEnabled(true) + .withFeatureKey("feature_1") + .withName("Feature 1") + .withValue("value_1") + .withReason("DEFAULT") + .withMetadata(Map.of("flagsmithId", 1))) + .withAdditionalProperty("feature_2", new FlagResult() + .withEnabled(false) + .withFeatureKey("feature_2") + .withName("Feature 2") + .withValue(null) + .withReason("DEFAULT") + ); + EvaluationResult evaluationResult = new EvaluationResult() + .withFlags(flagResults); + + Flags flags = Flags.fromEvaluationResult(evaluationResult, null, null); + Flag flag = (Flag) flags.getFlag("feature_1"); + + assertEquals(1, flags.getFlags().size()); + assertEquals(true, flag.getEnabled()); + assertEquals("value_1", flag.getValue()); + assertEquals("Feature 1", flag.getFeatureName()); + assertEquals(1, flag.getFeatureId().intValue()); + } +} From b5889e8ad367d12b318086f34da08df079e2b9f2 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 16 Oct 2025 18:38:38 +0100 Subject: [PATCH 52/62] fix --- src/test/java/com/flagsmith/FlagsmithTestHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index eaa2132f..ae1f5203 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -246,7 +246,7 @@ public static BaseFlag flag( result.setEnabled(enabled); result.setValue(value); - final FeatureModel feature = result.new FeatureModel(); + final FeatureModel feature = new FeatureModel(); feature.setName(name); feature.setType(type); From 3a05d8b77fd16afbf563666aa5cd950f4bd6f27d Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 12:30:11 +0100 Subject: [PATCH 53/62] fix `List` coercion --- .../java/com/flagsmith/flagengine/segments/SegmentEvaluator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index b98dd5db..b7bc0966 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -88,7 +88,6 @@ private static Boolean contextMatchesCondition( 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) { From ed5e516529fa6fb7772a2c2ceae59135de6f581b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 13:01:28 +0100 Subject: [PATCH 54/62] improve clarity for `IN` --- .../flagengine/segments/SegmentEvaluator.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index b7bc0966..214b4a1f 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -83,6 +83,10 @@ private static Boolean contextMatchesCondition( switch (operator) { case IN: + if (contextValue == null || contextValue instanceof Boolean) { + return false; + } + List conditionList = new ArrayList<>(); if (conditionValue instanceof List) { @@ -102,11 +106,7 @@ private static Boolean contextMatchesCondition( } } - if (!(contextValue instanceof Boolean) && contextValue != null) { - contextValue = String.valueOf(contextValue); - } - - return conditionList.contains(contextValue); + return conditionList.contains(String.valueOf(contextValue)); case PERCENTAGE_SPLIT: String key; From 23799bea36011732c21f626a8998a879ade40c29 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 13:13:51 +0100 Subject: [PATCH 55/62] remove extra hop --- src/main/java/com/flagsmith/FlagsmithApiWrapper.java | 9 +++++---- src/test/java/com/flagsmith/FlagsmithTestHelper.java | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index 4a955cbf..aa6b3f4e 100644 --- a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java +++ b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java @@ -11,6 +11,7 @@ import com.flagsmith.mappers.EngineMappers; import com.flagsmith.models.Flags; import com.flagsmith.models.TraitModel; +import com.flagsmith.models.environments.EnvironmentModel; import com.flagsmith.models.features.FeatureStateModel; import com.flagsmith.responses.FlagsAndTraitsResponse; import com.flagsmith.threads.AnalyticsProcessor; @@ -252,13 +253,13 @@ public Flags identifyUserWithTraits( public EvaluationContext getEvaluationContext() { final Request request = newGetRequest(defaultConfig.getEnvironmentUri()); - Future environmentFuture = requestor.executeAsync(request, - new TypeReference() {}, + Future environmentFuture = requestor.executeAsync(request, + new TypeReference() {}, Boolean.TRUE); try { - JsonNode environmentJson = environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); - return EngineMappers.mapEnvironmentDocumentToContext(environmentJson); + EnvironmentModel environment = environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS); + return EngineMappers.mapEnvironmentToContext(environment); } catch (TimeoutException ie) { logger.error("Timed out on fetching Feature flags.", ie); } catch (InterruptedException ie) { diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index ae1f5203..d1922d56 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -359,11 +359,7 @@ public static EnvironmentModel environmentModel() { } public static EvaluationContext evaluationContext() { - try { - return EngineMappers.mapEnvironmentDocumentToContext(MapperFactory.getMapper().readTree(environmentString())); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to parse environment JSON", e); - } + return EngineMappers.mapEnvironmentToContext(environmentModel()); } public static List getFlags() { From 988d59b68285766646237e59c9798b7c549c0e0c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 16:46:30 +0100 Subject: [PATCH 56/62] improve naming --- src/main/java/com/flagsmith/FlagsmithClient.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index ef1e1bc4..f9cb84c2 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -73,7 +73,7 @@ public void updateEnvironment() { */ public Flags getEnvironmentFlags() throws FlagsmithClientError { if (getShouldUseEnvironmentDocument()) { - return getEnvironmentFlagsFromDocument(); + return getEnvironmentFlagsFromEvaluationContext(); } return getEnvironmentFlagsFromApi(); @@ -137,7 +137,7 @@ public Flags getIdentityFlags(String identifier, Map traits) public Flags getIdentityFlags(String identifier, Map traits, boolean isTransient) throws FlagsmithClientError { if (getShouldUseEnvironmentDocument()) { - return getIdentityFlagsFromDocument(identifier, traits); + return getIdentityFlagsFromEvaluationContext(identifier, traits); } return getIdentityFlagsFromApi(identifier, traits, isTransient); @@ -207,7 +207,7 @@ public void close() { flagsmithSdk.close(); } - private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError { + private Flags getEnvironmentFlagsFromEvaluationContext() throws FlagsmithClientError { if (evaluationContext == null) { if (getConfig().getFlagsmithFlagDefaults() == null) { throw new FlagsmithClientError("Unable to get flags. No environment present."); @@ -223,7 +223,7 @@ private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError { getConfig().getFlagsmithFlagDefaults()); } - private Flags getIdentityFlagsFromDocument( + private Flags getIdentityFlagsFromEvaluationContext( String identifier, Map traits) throws FlagsmithClientError { if (evaluationContext == null) { @@ -252,7 +252,7 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError { return getDefaultFlags(); } else if (evaluationContext != null) { try { - return getEnvironmentFlagsFromDocument(); + return getEnvironmentFlagsFromEvaluationContext(); } catch (FlagsmithClientError ce) { // Do nothing and fall through to FlagsmithApiError } @@ -276,7 +276,7 @@ private Flags getIdentityFlagsFromApi( return getDefaultFlags(); } else if (evaluationContext != null) { try { - return getIdentityFlagsFromDocument(identifier, traits); + return getIdentityFlagsFromEvaluationContext(identifier, traits); } catch (FlagsmithClientError ce) { // Do nothing and fall through to FlagsmithApiError } From 2dfcbbced60ca760a876c5ee12dffd794c67c399 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 17:27:21 +0100 Subject: [PATCH 57/62] remove extra methods --- .../flagsmith/flagengine/IdentityContext.java | 31 ------------------- .../java/com/flagsmith/flagengine/Traits.java | 23 -------------- 2 files changed, 54 deletions(-) diff --git a/src/main/java/com/flagsmith/flagengine/IdentityContext.java b/src/main/java/com/flagsmith/flagengine/IdentityContext.java index 62329f3c..a7ba9f39 100644 --- a/src/main/java/com/flagsmith/flagengine/IdentityContext.java +++ b/src/main/java/com/flagsmith/flagengine/IdentityContext.java @@ -276,35 +276,4 @@ public String toString() { 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/Traits.java b/src/main/java/com/flagsmith/flagengine/Traits.java index 3cf83229..5db16c0e 100644 --- a/src/main/java/com/flagsmith/flagengine/Traits.java +++ b/src/main/java/com/flagsmith/flagengine/Traits.java @@ -83,27 +83,4 @@ public String toString() { } 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 From 183cdf9e102a448d1da2b4bc7ccb2f032078024d Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 17:36:59 +0100 Subject: [PATCH 58/62] remove null check --- src/main/java/com/flagsmith/mappers/EngineMappers.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 26087c31..575452d5 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -54,10 +54,6 @@ public static Flag mapFlagResultToFlag( metadata = MapperFactory.getMapper() .convertValue(flagResult.getMetadata(), FeatureMetadata.class); - - if (metadata == null || metadata.getFlagsmithId() == null) { - return null; - } Flag flag = new Flag(); flag.setFeatureId(metadata.getFlagsmithId()); From 7d1c948d24ae9239bcc267c519b91fd9da24ac40 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 22 Oct 2025 17:43:52 +0100 Subject: [PATCH 59/62] improve test --- .../com/flagsmith/flagengine/models/FlagsTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java b/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java index bde11bfd..11094c36 100644 --- a/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java +++ b/src/test/java/com/flagsmith/flagengine/models/FlagsTest.java @@ -28,17 +28,15 @@ public void testFromEvaluationResult__metadata__expected() throws FlagsmithClien .withName("Feature 2") .withValue(null) .withReason("DEFAULT") + .withMetadata(Map.of()) ); EvaluationResult evaluationResult = new EvaluationResult() .withFlags(flagResults); Flags flags = Flags.fromEvaluationResult(evaluationResult, null, null); - Flag flag = (Flag) flags.getFlag("feature_1"); - - assertEquals(1, flags.getFlags().size()); - assertEquals(true, flag.getEnabled()); - assertEquals("value_1", flag.getValue()); - assertEquals("Feature 1", flag.getFeatureName()); - assertEquals(1, flag.getFeatureId().intValue()); + + assertEquals(2, flags.getFlags().size()); + assertEquals(1, ((Flag) flags.getFlag("feature_1")).getFeatureId().intValue()); + assertEquals(null, ((Flag) flags.getFlag("feature_2")).getFeatureId()); } } From 9c46ddf97540e07416ec2c1287afc4634bfcdc61 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 23 Oct 2025 17:53:29 +0100 Subject: [PATCH 60/62] save some cycles --- src/main/java/com/flagsmith/FlagsmithClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index f9cb84c2..0d60232d 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -173,12 +173,13 @@ public List getIdentitySegments(String identifier, Map final EvaluationResult result = Engine.getEvaluationResult(context); + ObjectMapper mapper = MapperFactory.getMapper(); + return result.getSegments().stream().map((segmentModel) -> { if (segmentModel.getMetadata() == null) { return null; } - ObjectMapper mapper = MapperFactory.getMapper(); SegmentMetadata segmentMetadata = mapper.convertValue( segmentModel.getMetadata(), SegmentMetadata.class); From 96d6ca25f634cd29231448576208ef39c7e9d08e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 23 Oct 2025 17:58:25 +0100 Subject: [PATCH 61/62] encapsulate --- .../com/flagsmith/mappers/EngineMappers.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index 575452d5..f172316a 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -243,10 +243,7 @@ private static Map mapIdentityOverridesToSegments( SegmentMetadata metadata = new SegmentMetadata(); metadata.setSource(SegmentMetadata.Source.IDENTITY_OVERRIDES); - Map metadataMap = MapperFactory.getMapper() - .convertValue(metadata, - new com.fasterxml.jackson.core.type.TypeReference>() { - }); + Map metadataMap = mapSegmentMetadataToMetadataMap(metadata); SegmentContext segmentContext = new SegmentContext() .withKey("") // Identity override segments never use % Split operator @@ -416,10 +413,7 @@ private static SegmentContext mapSegmentToSegmentContext(SegmentModel segment) { metadata.setSource(SegmentMetadata.Source.API); metadata.setFlagsmithId(segment.getId()); - Map metadataMap = MapperFactory.getMapper() - .convertValue(metadata, - new com.fasterxml.jackson.core.type.TypeReference>() { - }); + Map metadataMap = mapSegmentMetadataToMetadataMap(metadata); String segmentKey = String.valueOf(segment.getId()); return new SegmentContext() @@ -456,4 +450,10 @@ private static String getVirtualSegmentKey( // This is safer than using List.hashCode() as we control the string content return String.valueOf(keyBuilder.toString().hashCode()); } + + private static Map mapSegmentMetadataToMetadataMap(SegmentMetadata metadata) { + return MapperFactory.getMapper().convertValue(metadata, + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + } } \ No newline at end of file From ccf7dd20b831637139b0ae1f29207ad891dac6e6 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 24 Oct 2025 12:27:03 +0100 Subject: [PATCH 62/62] improve logic, test coverage --- .../com/flagsmith/mappers/EngineMappers.java | 20 +++---- .../unit/mappers/EngineMappersTest.java | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/test/java/com/flagsmith/flagengine/unit/mappers/EngineMappersTest.java diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java index f172316a..51ec7eef 100644 --- a/src/main/java/com/flagsmith/mappers/EngineMappers.java +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -18,6 +18,7 @@ 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; @@ -28,6 +29,7 @@ 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; @@ -79,22 +81,16 @@ public static EvaluationContext mapContextAndIdentityDataToContext( // Create identity context IdentityContext identityContext = new IdentityContext() .withIdentifier(identifier) - .withKey(context.getEnvironment().getKey() + "_" + identifier) - .withTraits(new Traits()); + .withKey(context.getEnvironment().getKey() + "_" + identifier); // 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); + 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 diff --git a/src/test/java/com/flagsmith/flagengine/unit/mappers/EngineMappersTest.java b/src/test/java/com/flagsmith/flagengine/unit/mappers/EngineMappersTest.java new file mode 100644 index 00000000..27e59b7f --- /dev/null +++ b/src/test/java/com/flagsmith/flagengine/unit/mappers/EngineMappersTest.java @@ -0,0 +1,52 @@ +package com.flagsmith.flagengine.unit.mappers; + +import com.flagsmith.mappers.EngineMappers; +import com.flagsmith.models.TraitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.flagsmith.FlagsmithTestHelper; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.Traits; + +public class EngineMappersTest { + private static Stream expectedTraitMaps() { + return Stream.of( + Arguments.argumentSet( + "no transiency data", + Map.of("test", 1) + ), + Arguments.argumentSet( + "with transiency data", + Map.of("test", TraitConfig.fromObject(1)) + ) + ); + } + + @ParameterizedTest + @MethodSource("expectedTraitMaps") + public void testMapContextAndIdentityDataToContext_returnsExpectedContext( + Map expectedTraitMap + ) { + // Arrange + final String identifier = "test-identifier"; + final EvaluationContext context = FlagsmithTestHelper.evaluationContext(); + final Traits expectedTraits = new Traits().withAdditionalProperty("test", 1); + + // Act + final EvaluationContext mappedContext = EngineMappers.mapContextAndIdentityDataToContext( + context, identifier, expectedTraitMap); + + // Assert + assertEquals( + expectedTraits.getAdditionalProperties(), + mappedContext.getIdentity().getTraits().getAdditionalProperties()); + } +}