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/.gitmodules b/.gitmodules index b617d21b..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 - branch = v1.0.0 + tag = v2.5.0 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0b892651..bafddad3 100644 --- a/pom.xml +++ b/pom.xml @@ -38,14 +38,16 @@ + 11 + 11 UTF-8 UTF-8 - 1.11 + 11 2.15.2 1.18.34 1.7.30 3.4.0 - 5.9.2 + 5.14.0 @@ -54,6 +56,11 @@ okhttp 4.12.0 + + com.jayway.jsonpath + json-path + 2.9.0 + com.fasterxml.jackson.core jackson-annotations @@ -181,6 +188,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 @@ -196,8 +224,15 @@ maven-compiler-plugin 3.7.0 - 1.8 - 1.8 + 11 + 11 + + + org.projectlombok + lombok + ${lombok.version} + + @@ -257,7 +292,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.0 + 3.5.4 diff --git a/src/main/java/com/flagsmith/FlagsmithApiWrapper.java b/src/main/java/com/flagsmith/FlagsmithApiWrapper.java index b327ad95..4a955cbf 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.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; @@ -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; @@ -248,19 +249,22 @@ 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); diff --git a/src/main/java/com/flagsmith/FlagsmithClient.java b/src/main/java/com/flagsmith/FlagsmithClient.java index 08801ab8..ef1e1bc4 100644 --- a/src/main/java/com/flagsmith/FlagsmithClient.java +++ b/src/main/java/com/flagsmith/FlagsmithClient.java @@ -1,29 +1,27 @@ package com.flagsmith; +import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.config.FlagsmithCacheConfig; import com.flagsmith.config.FlagsmithConfig; import com.flagsmith.exceptions.FlagsmithApiError; 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.Flags; -import com.flagsmith.models.SdkTraitModel; import com.flagsmith.models.Segment; +import com.flagsmith.models.SegmentMetadata; 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; +import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import lombok.Data; @@ -39,9 +37,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 +52,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 +137,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,24 +164,36 @@ 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) -> { + 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(segmentModel.getId()); + segment.setId(flagsmithId); segment.setName(segmentModel.getName()); return segment; - }).collect(Collectors.toList()); + }).filter(Objects::nonNull).collect(Collectors.toList()); } /** @@ -212,37 +208,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); + + final EvaluationResult result = Engine.getEvaluationResult(context); - return Flags.fromFeatureStateModels( - featureStates, + return Flags.fromEvaluationResult( + result, getConfig().getAnalyticsProcessor(), - identity.getCompositeKey(), getConfig().getFlagsmithFlagDefaults()); } @@ -252,7 +250,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 +263,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 +286,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 +293,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 { @@ -574,7 +549,8 @@ public FlagsmithClient build() { throw new FlagsmithRuntimeError( "Cannot use both default flag handler and offline handler."); } - client.environment = configuration.getOfflineHandler().getEnvironment(); + client.evaluationContext = EngineMappers.mapEnvironmentToContext( + configuration.getOfflineHandler().getEnvironment()); } return this.client; diff --git a/src/main/java/com/flagsmith/flagengine/Engine.java b/src/main/java/com/flagsmith/flagengine/Engine.java index ec9beb46..f886ce2e 100644 --- a/src/main/java/com/flagsmith/flagengine/Engine.java +++ b/src/main/java/com/flagsmith/flagengine/Engine.java @@ -1,153 +1,169 @@ 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. - * - * @param environment Instance of the Environment. - */ - public static List getEnvironmentFeatureStates(EnvironmentModel environment) { - if (environment.getProject().getHideDisabledFlags()) { - return environment.getFeatureStates() - .stream() - .filter((featureState) -> featureState.getEnabled()) - .collect(Collectors.toList()); + private static class SegmentEvaluationResult { + List segments; + HashMap> segmentFeatureContexts; + + public SegmentEvaluationResult( + List segments, + HashMap> segmentFeatureContexts) { + this.segments = segments; + this.segmentFeatureContexts = segmentFeatureContexts; } - 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); - } + public List getSegments() { + return segments; + } - /** - * 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()); + public HashMap> getSegmentFeatureContexts() { + return segmentFeatureContexts; } - return featureStates; } /** - * Get a specific feature state for a given identity in a given environment. + * Get evaluation result for a given evaluation context. * - * @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. + * @param context Evaluation context. + * @return Evaluation result. */ - 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); + public static EvaluationResult getEvaluationResult(EvaluationContext context) { + SegmentEvaluationResult segmentEvaluationResult = evaluateSegments(context); + Flags flags = evaluateFeatures(context, segmentEvaluationResult.getSegmentFeatureContexts()); + + return new EvaluationResult() + .withFlags(flags) + .withSegments(segmentEvaluationResult.getSegments()); } - /** - * 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) - ); + private static SegmentEvaluationResult evaluateSegments( + EvaluationContext context) { + List segments = new ArrayList<>(); + HashMap> segmentFeatureContexts = new HashMap<>(); + + 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)); + } + } + } + } } - List identitySegments = - SegmentEvaluator.getIdentitySegments(environmentModel, identityModel, overrideTraits); + return new SegmentEvaluationResult(segments, segmentFeatureContexts); + } - 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 Flags evaluateFeatures( + EvaluationContext context, + HashMap> segmentFeatureContexts) { + Features contextFeatures = context.getFeatures(); + Flags flags = new Flags(); + + String identityKey = context.getIdentity() != null + ? context.getIdentity().getKey() + : null; + + 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()) + .withMetadata(featureContext.getMetadata())); + } else { + flags.setAdditionalProperty(featureContext.getName(), + getFlagResultFromFeatureContext(featureContext, identityKey)); } - - featureStates.put(featureState.getFeature(), featureState); } } - for (FeatureStateModel featureState : identityModel.getIdentityFeatures()) { - if (featureStates.containsKey(featureState.getFeature())) { - featureStates.put(featureState.getFeature(), featureState); + return flags; + } + + 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; + + 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) { + return new FlagResult().withEnabled(featureContext.getEnabled()) + .withFeatureKey(featureContext.getFeatureKey()) + .withName(featureContext.getName()) + .withValue(variant.getValue()) + .withReason("SPLIT; weight=" + weight.intValue()) + .withMetadata(featureContext.getMetadata()); + } + startPercentage = limit; + } } } - return featureStates; + return new FlagResult().withEnabled(featureContext.getEnabled()) + .withFeatureKey(featureContext.getFeatureKey()) + .withName(featureContext.getName()) + .withValue(featureContext.getValue()) + .withReason("DEFAULT") + .withMetadata(featureContext.getMetadata()); } } \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/EngineConstants.java b/src/main/java/com/flagsmith/flagengine/EngineConstants.java new file mode 100644 index 00000000..cd172485 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/EngineConstants.java @@ -0,0 +1,6 @@ +package com.flagsmith.flagengine; + +public class EngineConstants { + public static final double STRONGEST_PRIORITY = Double.NEGATIVE_INFINITY; + public static final double WEAKEST_PRIORITY = Double.POSITIVE_INFINITY; +} diff --git a/src/main/java/com/flagsmith/flagengine/IdentityContext.java b/src/main/java/com/flagsmith/flagengine/IdentityContext.java new file mode 100644 index 00000000..62329f3c --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/IdentityContext.java @@ -0,0 +1,310 @@ +/* + * IdentityContext + * + *

This class was auto-generated by jsonschema2pojo.org + * and is referenced in the Evaluation Context schema as + * `"existingJavaType": "com.flagsmith.flagengine.IdentityContext"` + * to work around jsonschema2pojo's lack of `anyOf` support. + */ + +package com.flagsmith.flagengine; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * IdentityContext + * + *

Represents an identity context for feature flag evaluation. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "identifier", + "key", + "traits" +}) +public class IdentityContext { + + /** + * Identifier + * + *

A unique identifier for an identity, used for segment and multivariate + * feature flag targeting, and displayed in the Flagsmith UI. + * (Required) + */ + @JsonProperty("identifier") + @JsonPropertyDescription("A unique identifier for an identity, used for segment and multivariate " + + "feature flag targeting, and displayed in the Flagsmith UI.") + private String identifier; + + /** + * Key + * + *

Key used when selecting a value for a multivariate feature, or for % split + * segmentation. Set to an internal identifier or a composite value based on the + * environment key and identifier, depending on Flagsmith implementation. + * (Required) + */ + @JsonProperty("key") + @JsonPropertyDescription("Key used when selecting a value for a multivariate feature, " + + "or for % split segmentation. Set to an internal identifier or a composite value " + + "based on the environment key and identifier, depending on Flagsmith implementation.") + private String key; + + /** + * Traits + * + *

A map of traits associated with the identity, where the key is the trait name + * and the value is the trait value. + */ + @JsonProperty("traits") + @JsonPropertyDescription("A map of traits associated with the identity, " + + "where the key is the trait name and the value is the trait value.") + private Traits traits; + + @JsonIgnore + private Map additionalProperties = new LinkedHashMap(); + + /** + * No args constructor for use in serialization. + */ + public IdentityContext() { + } + + /** + * Copy constructor. + * + * @param source the object being copied + */ + public IdentityContext(IdentityContext source) { + super(); + this.identifier = source.identifier; + this.key = source.key; + this.traits = source.traits; + } + + /** + * Constructor with required fields. + * + * @param identifier A unique identifier for an identity + * @param key Key used when selecting a value for a multivariate feature + */ + public IdentityContext(String identifier, String key) { + this.identifier = identifier; + this.key = key; + } + + /** + * Constructor with all fields. + * + * @param identifier A unique identifier for an identity + * @param key Key used when selecting a value for a multivariate feature + * @param traits A map of traits associated with the identity + */ + public IdentityContext(String identifier, String key, Traits traits) { + this.identifier = identifier; + this.key = key; + this.traits = traits; + } + + /** + * Identifier + * + *

A unique identifier for an identity, used for segment and multivariate + * feature flag targeting, and displayed in the Flagsmith UI. + * (Required) + */ + @JsonProperty("identifier") + public String getIdentifier() { + return identifier; + } + + /** + * Identifier + * + *

A unique identifier for an identity, used for segment and multivariate + * feature flag targeting, and displayed in the Flagsmith UI. + * (Required) + */ + @JsonProperty("identifier") + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * Fluent setter for identifier. + * + * @param identifier A unique identifier for an identity + * @return the IdentityContext instance + */ + public IdentityContext withIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + + /** + * Key + * + *

Key used when selecting a value for a multivariate feature, or for % split + * segmentation. Set to an internal identifier or a composite value based on the + * environment key and identifier, depending on Flagsmith implementation. + * (Required) + */ + @JsonProperty("key") + public String getKey() { + return key; + } + + /** + * Key + * + *

Key used when selecting a value for a multivariate feature, or for % split + * segmentation. Set to an internal identifier or a composite value based on the + * environment key and identifier, depending on Flagsmith implementation. + * (Required) + */ + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + * Fluent setter for key. + * + * @param key the key + * @return the IdentityContext instance + */ + public IdentityContext withKey(String key) { + this.key = key; + return this; + } + + /** + * Traits + * + *

A map of traits associated with the identity, where the key is the trait name + * and the value is the trait value. + */ + @JsonProperty("traits") + public Traits getTraits() { + return traits; + } + + /** + * Traits + * + *

A map of traits associated with the identity, where the key is the trait name + * and the value is the trait value. + */ + @JsonProperty("traits") + public void setTraits(Traits traits) { + this.traits = traits; + } + + /** + * Fluent setter for traits. + * + * @param traits A map of traits associated with the identity + * @return the IdentityContext instance + */ + public IdentityContext withTraits(Traits traits) { + this.traits = traits; + return this; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + /** + * Set additional property. + * + * @param name the name + * @param value the value + */ + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + /** + * Fluent setter for additional properties. + * + * @param name the name of the additional property + * @param value the value of the additional property + * @return the IdentityContext instance + */ + public IdentityContext withAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + return this; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(IdentityContext.class.getName()).append('@') + .append(Integer.toHexString(System.identityHashCode(this))).append('['); + sb.append("identifier"); + sb.append('='); + sb.append(((this.identifier == null) ? "" : this.identifier)); + sb.append(','); + sb.append("key"); + sb.append('='); + sb.append(((this.key == null) ? "" : this.key)); + sb.append(','); + sb.append("traits"); + sb.append('='); + sb.append(((this.traits == null) ? "" : this.traits)); + sb.append(','); + sb.append("additionalProperties"); + sb.append('='); + sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties)); + sb.append(','); + if (sb.charAt((sb.length() - 1)) == ',') { + sb.setCharAt((sb.length() - 1), ']'); + } else { + sb.append(']'); + } + return sb.toString(); + } + + @Override + public int hashCode() { + int result = 1; + result = ((result * 31) + ((this.identifier == null) ? 0 : this.identifier.hashCode())); + result = ((result * 31) + ((this.traits == null) ? 0 : this.traits.hashCode())); + result = ((result * 31) + ((this.additionalProperties == null) ? 0 + : this.additionalProperties.hashCode())); + result = ((result * 31) + ((this.key == null) ? 0 : this.key.hashCode())); + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if ((other instanceof IdentityContext) == false) { + return false; + } + IdentityContext rhs = ((IdentityContext) other); + return (((((this.identifier == rhs.identifier) + || ((this.identifier != null) && this.identifier.equals(rhs.identifier))) + && ((this.traits == rhs.traits) || ((this.traits != null) + && this.traits.equals(rhs.traits)))) + && ((this.additionalProperties == rhs.additionalProperties) + || ((this.additionalProperties != null) + && this.additionalProperties.equals(rhs.additionalProperties)))) + && ((this.key == rhs.key) || ((this.key != null) + && this.key.equals(rhs.key)))); + } + +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/SegmentCondition.java b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java new file mode 100644 index 00000000..892a5c80 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/SegmentCondition.java @@ -0,0 +1,154 @@ +package com.flagsmith.flagengine; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import lombok.Data; + +@Data +public class SegmentCondition { + private SegmentConditions operator; + private Object value; + private String property; + + /** + * No args constructor for use in serialization. + */ + public SegmentCondition() { + } + + /** + * Copy constructor. + * + * @param source the object being copied + */ + public SegmentCondition(SegmentCondition source) { + super(); + this.operator = source.operator; + this.value = source.value; + this.property = source.property; + } + + /** + * Constructor with all fields. + * + * @param operator the segment condition operator + * @param property the property name + * @param value the condition value + */ + public SegmentCondition(SegmentConditions operator, String property, Object value) { + this.operator = operator; + this.property = property; + this.value = value; + } + + /** + * Set JsonNode value. Required for engine tests. + * + * @param node the JsonNode value. + * @throws IllegalArgumentException if value is not a String or List of Strings + */ + @JsonSetter("value") + public void setValue(JsonNode node) { + if (node.isArray()) { + ArrayNode arr = (ArrayNode) node; + List values = StreamSupport.stream(arr.spliterator(), false) + .peek(el -> { + if (!el.isTextual()) { + throw new IllegalArgumentException("Array elements must be strings"); + } + }) + .map(JsonNode::asText) + .collect(Collectors.toList()); + this.setValue(values); + } else if (node.isTextual()) { + this.setValue(node.asText()); + } else { + throw new IllegalArgumentException("Value must be a String or List of Strings"); + } + } + + /** + * Set String value. + * + * @param value New String value. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Set List value. + * + * @param value New List value. + */ + public void setValue(List value) { + if (this.operator != SegmentConditions.IN) { + throw new IllegalArgumentException("List value can only be set for IN operator"); + } + this.value = value; + } + + /** + * Fluent setter for operator. + * + * @param operator the segment condition operator + * @return the SegmentCondition instance + */ + public SegmentCondition withOperator(SegmentConditions operator) { + this.operator = operator; + return this; + } + + /** + * Fluent setter for property. + * + * @param property the property name + * @return the SegmentCondition instance + */ + public SegmentCondition withProperty(String property) { + this.property = property; + return this; + } + + /** + * Fluent setter for value. + * + * @param value the condition value + * @return the SegmentCondition instance + */ + public SegmentCondition withValue(Object value) { + this.value = value; + return this; + } + + /** + * Fluent setter for String value. + * + * @param value the String condition value + * @return the SegmentCondition instance + */ + public SegmentCondition withValue(String value) { + this.value = value; + return this; + } + + /** + * Fluent setter for List value. + * + * @param value the List condition value + * @return the SegmentCondition instance + * @throws IllegalArgumentException if operator is not IN + */ + public SegmentCondition withValue(List value) { + if (this.operator != SegmentConditions.IN) { + throw new IllegalArgumentException("List value can only be set for IN operator"); + } + this.value = value; + return this; + } +} diff --git a/src/main/java/com/flagsmith/flagengine/Traits.java b/src/main/java/com/flagsmith/flagengine/Traits.java new file mode 100644 index 00000000..3cf83229 --- /dev/null +++ b/src/main/java/com/flagsmith/flagengine/Traits.java @@ -0,0 +1,109 @@ +package com.flagsmith.flagengine; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Traits + * + *

A map of traits associated with the identity, where the key is the trait name + * and the value is the trait value. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + +}) +public class Traits { + + @JsonIgnore + private Map additionalProperties = new LinkedHashMap(); + + /** + * No args constructor for use in serialization. + */ + public Traits() { + } + + /** + * Copy constructor. + * + * @param source the object being copied + */ + public Traits(Traits source) { + super(); + this.additionalProperties = new LinkedHashMap<>(source.additionalProperties); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + /** + * Set additional property. + * + * @param name the name + * @param value the value + */ + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + /** + * Fluent setter for additional property. + * + * @param name the name + * @param value the value + * @return the Traits instance + */ + public Traits withAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + return this; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(Traits.class.getName()).append('@') + .append(Integer.toHexString(System.identityHashCode(this))).append('['); + sb.append("additionalProperties"); + sb.append('='); + sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties)); + sb.append(','); + if (sb.charAt((sb.length() - 1)) == ',') { + sb.setCharAt((sb.length() - 1), ']'); + } else { + sb.append(']'); + } + return sb.toString(); + } + + @Override + public int hashCode() { + int result = 1; + result = ((result * 31) + ((this.additionalProperties == null) ? 0 + : this.additionalProperties.hashCode())); + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if ((other instanceof Traits) == false) { + return false; + } + Traits rhs = ((Traits) other); + return ((this.additionalProperties == rhs.additionalProperties) + || ((this.additionalProperties != null) + && this.additionalProperties.equals(rhs.additionalProperties))); + } + +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java b/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java deleted file mode 100644 index c210771a..00000000 --- a/src/main/java/com/flagsmith/flagengine/environments/integrations/IntegrationModel.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.flagsmith.flagengine.environments.integrations; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -@Data -public class IntegrationModel { - @JsonProperty("api_key") - private String apiKey; - @JsonProperty("base_url") - private String baseUrl; -} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java deleted file mode 100644 index 4b6a7cdd..00000000 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureModel.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.flagsmith.flagengine.features; - -import lombok.Data; - -@Data -public class FeatureModel { - private Integer id; - private String name; - private String type; - - @Override - public boolean equals(Object o) { - if (!(o instanceof FeatureModel)) { - return false; - } - - return id != null && id.equals(((FeatureModel) o).getId()); - } - - @Override - public int hashCode() { - return id.hashCode(); - } -} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java deleted file mode 100644 index 5118dd91..00000000 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureSegmentModel.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.flagsmith.flagengine.features; - -import com.flagsmith.flagengine.utils.models.BaseModel; -import lombok.Data; - -@Data -public class FeatureSegmentModel extends BaseModel { - private Integer priority; - - public FeatureSegmentModel() { - this.priority = 0; - } - - public FeatureSegmentModel(Integer priority) { - this.priority = priority; - } -} diff --git a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java b/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java deleted file mode 100644 index 1cec6f17..00000000 --- a/src/main/java/com/flagsmith/flagengine/features/FeatureStateModel.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.flagsmith.flagengine.features; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.utils.Hashing; -import com.flagsmith.utils.models.BaseModel; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; -import lombok.Data; - -@Data -public class FeatureStateModel extends BaseModel { - private FeatureModel feature; - private Boolean enabled; - @JsonProperty("django_id") - private Integer djangoId; - @JsonProperty("featurestate_uuid") - private String featurestateUuid = UUID.randomUUID().toString(); - @JsonProperty("multivariate_feature_state_values") - private List multivariateFeatureStateValues; - @JsonProperty("feature_state_value") - private Object value; - @JsonProperty("feature_segment") - private FeatureSegmentModel featureSegment; - - /** - * Returns the value object. - * - * @param identityId Identity ID - */ - public Object getValue(Object identityId) { - - if (identityId != null && multivariateFeatureStateValues != null - && multivariateFeatureStateValues.size() > 0) { - return getMultiVariateValue(identityId); - } - - return value; - } - - /** - * Determines the multi variate value. - * - * @param identityId Identity ID - */ - private Object getMultiVariateValue(Object identityId) { - - List objectIds = Arrays.asList( - (djangoId != null && djangoId != 0 ? djangoId.toString() : featurestateUuid), - identityId.toString() - ); - - Float percentageValue = Hashing.getInstance().getHashedPercentageForObjectIds(objectIds); - Float startPercentage = 0f; - - List sortedMultiVariateFeatureStates = - multivariateFeatureStateValues - .stream() - .sorted((smvfs1, smvfs2) -> smvfs1.getSortValue().compareTo(smvfs2.getSortValue())) - .collect(Collectors.toList()); - - for (MultivariateFeatureStateValueModel multiVariate : sortedMultiVariateFeatureStates) { - Float limit = multiVariate.getPercentageAllocation() + startPercentage; - - if (startPercentage <= percentageValue && percentageValue < limit) { - return multiVariate.getMultivariateFeatureOption().getValue(); - } - - startPercentage = limit; - } - - return value; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof FeatureStateModel)) { - return false; - } - - return this.getFeature().getId() == ((FeatureStateModel) o).getFeature().getId(); - } - - /** - * Another FeatureStateModel is deemed to be higher priority if and only if - * it has a FeatureSegment and either this.FeatureSegment is null or the - * value of other.FeatureSegment.priority is lower than that of - * this.FeatureSegment.priority. - * - * @param other the other FeatureStateModel to compare priority with - * @return true if `this` is higher priority than `other` - */ - public boolean isHigherPriority(FeatureStateModel other) { - if (this.featureSegment == null || other.featureSegment == null) { - return this.featureSegment != null && other.featureSegment == null; - } - - return this.featureSegment.getPriority() < other.featureSegment.getPriority(); - } -} diff --git a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java b/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java deleted file mode 100644 index d7aa5902..00000000 --- a/src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.flagsmith.flagengine.identities; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import com.flagsmith.utils.models.BaseModel; -import java.sql.Date; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import lombok.Data; - -@Data -public class IdentityModel extends BaseModel { - @JsonProperty("django_id") - private Integer djangoId; - private String identifier; - @JsonProperty("environment_api_key") - private String environmentApiKey; - @JsonProperty("created_date") - private Date createdDate; - @JsonProperty("identity_uuid") - private String identityUuid = UUID.randomUUID().toString(); - @JsonProperty("identity_traits") - private List 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/organisations/OrganisationModel.java b/src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java deleted file mode 100644 index eaacda9b..00000000 --- a/src/main/java/com/flagsmith/flagengine/organisations/OrganisationModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.flagsmith.flagengine.organisations; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.utils.models.BaseModel; -import lombok.Data; - -@Data -public class OrganisationModel extends BaseModel { - private Integer id; - private String name; - @JsonProperty("feature_analytics") - private Boolean featureAnalytics; - @JsonProperty("stop_serving_flags") - private Boolean stopServingFlags; - @JsonProperty("persist_trait_data") - private Boolean persistTraitData; - - public String uniqueSlug() { - return id.toString() + "-" + name; - } -} diff --git a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java b/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java deleted file mode 100644 index 2e00dd31..00000000 --- a/src/main/java/com/flagsmith/flagengine/projects/ProjectModel.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.flagsmith.flagengine.projects; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.flagsmith.flagengine.organisations.OrganisationModel; -import com.flagsmith.flagengine.segments.SegmentModel; -import com.flagsmith.utils.models.BaseModel; -import java.util.List; -import lombok.Data; - -@Data -public class ProjectModel extends BaseModel { - private Integer id; - private String name; - @JsonProperty("hide_disabled_flags") - private Boolean hideDisabledFlags; - private OrganisationModel organisation; - private List segments; -} diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java index c0b2856e..b98dd5db 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java +++ b/src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java @@ -1,196 +1,212 @@ package com.flagsmith.flagengine.segments; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.SegmentCondition; +import com.flagsmith.flagengine.SegmentContext; +import com.flagsmith.flagengine.SegmentRule; import com.flagsmith.flagengine.segments.constants.SegmentConditions; import com.flagsmith.flagengine.utils.Hashing; import com.flagsmith.flagengine.utils.types.TypeCasting; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; public class SegmentEvaluator { + private static ObjectMapper mapper = new ObjectMapper(); + private static Configuration jsonPathConfiguration = Configuration + .defaultConfiguration() + .setOptions(Option.SUPPRESS_EXCEPTIONS); + private static TypeReference> stringListTypeRef = new TypeReference>() { + }; /** - * Get segment identities from environment and identity. + * Check if context is in segment. * - * @param environment Environment instance. - * @param identity Identity Instance. + * @param context Evaluation context. + * @param segment Segment context. + * @return true if context is in segment. */ - public static List getIdentitySegments(EnvironmentModel environment, - IdentityModel identity) { - return getIdentitySegments(environment, identity, null); + public static Boolean isContextInSegment(EvaluationContext context, SegmentContext segment) { + List rules = segment.getRules(); + return !rules.isEmpty() && rules.stream() + .allMatch((rule) -> contextMatchesRule(context, rule, segment.getKey())); } - /** - * Get segment identities from environment and identity along with traits to override. - * - * @param environment Environment Instance. - * @param identity Identity Instance. - * @param overrideTraits Traits to over ride. - */ - public static List getIdentitySegments(EnvironmentModel environment, - IdentityModel identity, - List overrideTraits) { - return environment - .getProject() - .getSegments() - .stream() - .filter((segment) -> evaluateIdentityInSegment(identity, segment, overrideTraits)) - .collect(Collectors.toList()); - } - - /** - * Evaluate the traits in identities and overrides with rules from segments. - * - * @param identity Identity instance. - * @param segment Segment Instance. - * @param overrideTraits Overriden traits. - */ - public static Boolean evaluateIdentityInSegment(IdentityModel identity, SegmentModel segment, - List 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) { + Predicate conditionPredicate = (condition) -> contextMatchesCondition( + context, condition, segmentKey); + + Boolean isMatch; + List conditions = rule.getConditions(); + + if (conditions.isEmpty()) { + isMatch = true; + } else { + switch (rule.getType()) { + case ALL: + isMatch = conditions.stream().allMatch(conditionPredicate); + break; + case ANY: + isMatch = conditions.stream().anyMatch(conditionPredicate); + break; + case NONE: + isMatch = conditions.stream().noneMatch(conditionPredicate); + break; + default: + return false; + } } - return Boolean.FALSE; + return isMatch && rule.getRules().stream() + .allMatch((subRule) -> contextMatchesRule(context, subRule, segmentKey)); } - /** - * Evaluate whether the trait match the rule from segment. - * - * @param identityTraits Traits to match against. - * @param rule Rule from segments to evaluate with. - * @param segmentId Segment ID (for hashing) - * @param identityId Identity ID (for hashing) - */ - private static Boolean traitsMatchSegmentRule(List identityTraits, - SegmentRuleModel rule, - Integer segmentId, String identityId) { - Boolean matchingCondition = Boolean.TRUE; - - if (rule.getConditions() != null && rule.getConditions().size() > 0) { - List conditionEvaluations = rule.getConditions().stream() - .map((condition) -> traitsMatchSegmentCondition(identityTraits, condition, segmentId, - identityId)) - .collect(Collectors.toList()); - - matchingCondition = rule.matchingFunction( - conditionEvaluations.stream() - ); - } + private static Boolean contextMatchesCondition( + EvaluationContext context, + SegmentCondition condition, + String segmentKey) { + Object contextValue = getContextValue(context, condition.getProperty()); + Object conditionValue = condition.getValue(); + SegmentConditions operator = condition.getOperator(); - List rules = rule.getRules(); + switch (operator) { + case IN: + List conditionList = new ArrayList<>(); + + if (conditionValue instanceof List) { + List maybeConditionList = (List) conditionValue; + conditionList = maybeConditionList.stream() + .filter(String.class::isInstance) + .map(Object::toString) + .collect(Collectors.toList()); + } else if (conditionValue instanceof String) { + String stringConditionValue = (String) conditionValue; + try { + // Try parsing a JSON list first + conditionList = mapper.readValue( + stringConditionValue, stringListTypeRef); + } catch (IOException e) { + // As a fallback, split by comma + conditionList = Arrays.asList(stringConditionValue.split(",")); + } + } - if (rules != null) { - matchingCondition = matchingCondition && rules.stream() - .allMatch((segmentRule) -> traitsMatchSegmentRule( - identityTraits, - segmentRule, - segmentId, - identityId - )); - } + if (!(contextValue instanceof Boolean) && contextValue != null) { + contextValue = String.valueOf(contextValue); + } - return matchingCondition; - } + return conditionList.contains(contextValue); + + case PERCENTAGE_SPLIT: + String key; + if (contextValue == null) { + if (context.getIdentity() == null) { + return false; + } + key = context.getIdentity().getKey(); + } else { + key = contextValue.toString(); + } + List objectIds = List.of(segmentKey, key); - /** - * Evaluate traits and compare them with condition. - * - * @param identityTraits Traits to match against. - * @param condition Condition to evaluate with. - * @param segmentId Segment ID (for hashing) - * @param identityId Identity ID (for hashing) - */ - private static Boolean traitsMatchSegmentCondition(List identityTraits, - SegmentConditionModel condition, - Integer segmentId, String identityId) { - if (condition.getOperator().equals(SegmentConditions.PERCENTAGE_SPLIT)) { - try { - Float floatValue = Float.parseFloat(condition.getValue()); - return Hashing.getInstance().getHashedPercentageForObjectIds( - Arrays.asList(segmentId.toString(), identityId)) <= floatValue; - - } catch (NumberFormatException nfe) { - return Boolean.FALSE; - } - } + final float floatValue; + try { + floatValue = Float.parseFloat(String.valueOf(condition.getValue())); + } catch (NumberFormatException e) { + return false; + } - if (identityTraits != null) { - Optional matchingTrait = identityTraits - .stream() - .filter((trait) -> trait.getTraitKey().equals(condition.getProperty_())) - .findFirst(); + return Hashing.getInstance() + .getHashedPercentageForObjectIds(objectIds) <= floatValue; - return traitMatchesSegmentCondition(matchingTrait, condition); - } + case IS_NOT_SET: + return contextValue == null; - return condition.getOperator().equals(SegmentConditions.IS_NOT_SET); - } + case IS_SET: + return contextValue != null; - /** - * Evaluate a single trait and compare it with condition. - * - * @param trait Trait to match against. - * @param condition Condition to evaluate with. - */ - private static Boolean traitMatchesSegmentCondition(Optional trait, - SegmentConditionModel condition) { - if (condition.getOperator().equals(SegmentConditions.IS_NOT_SET)) { - return !trait.isPresent(); - } else if (condition.getOperator().equals(SegmentConditions.IS_SET)) { - return trait.isPresent(); - } - - return trait.isPresent() && conditionMatchesTraitValue(condition, trait.get().getTraitValue()); - } + case CONTAINS: + return (String.valueOf(contextValue)).indexOf(conditionValue.toString()) > -1; - /** - * Matches condition value with the trait value. - * - * @param condition Condition to evaluate with. - * @param value Trait value to compare with. - */ - public static Boolean conditionMatchesTraitValue(SegmentConditionModel condition, Object value) { - SegmentConditions operator = condition.getOperator(); - switch (operator) { case NOT_CONTAINS: - return (String.valueOf(value)).indexOf(condition.getValue()) == -1; - case CONTAINS: - return (String.valueOf(value)).indexOf(condition.getValue()) > -1; - case IN: - if (value instanceof String) { - return Arrays.asList(condition.getValue().split(",")).contains(value); - } - if (value instanceof Integer) { - return Arrays.asList(condition.getValue().split(",")).contains(String.valueOf(value)); + if (contextValue != null) { + return (String.valueOf(contextValue)).indexOf(conditionValue.toString()) == -1; } return false; + case REGEX: - Pattern pattern = Pattern.compile(condition.getValue()); - return pattern.matcher(String.valueOf(value)).find(); + if (contextValue != null) { + try { + Pattern pattern = Pattern.compile(conditionValue.toString()); + return pattern.matcher(contextValue.toString()).find(); + } catch (PatternSyntaxException pse) { + return false; + } + } + return false; + + case MODULO: + if (contextValue instanceof Number && conditionValue instanceof String) { + try { + String[] parts = conditionValue.toString().split("\\|"); + if (parts.length != 2) { + return false; + } + Double divisor = Double.parseDouble(parts[0]); + Double remainder = Double.parseDouble(parts[1]); + Double value = ((Number) contextValue).doubleValue(); + return (value % divisor) == remainder; + } catch (NumberFormatException nfe) { + return false; + } + } + return false; + default: - return TypeCasting.compare(operator, value, condition.getValue()); + if (contextValue == null) { + return false; + } + return TypeCasting.compare(operator, contextValue, conditionValue); + } + } + + /** + * Get context value by property name. + * + * @param context Evaluation context. + * @param property Property name. + * @return Property value. + */ + private static Object getContextValue(EvaluationContext context, String property) { + Object result; + if (context.getIdentity() != null && context.getIdentity().getTraits() != null) { + result = context.getIdentity().getTraits().getAdditionalProperties().get(property); + if (result != null) { + return result; + } + } + if (property.startsWith("$.")) { + result = JsonPath + .using(jsonPathConfiguration) + .parse(mapper.convertValue(context, Map.class)) + .read(property); + if (result instanceof List || result instanceof Map) { + return null; + } + return result; } + return null; } } \ No newline at end of file diff --git a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java b/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java deleted file mode 100644 index 39a6d0d6..00000000 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentRuleModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.flagsmith.flagengine.segments; - -import com.flagsmith.flagengine.segments.constants.SegmentRules; -import java.util.List; -import java.util.stream.Stream; -import lombok.Data; - -@Data -public class SegmentRuleModel { - private String type; - private List rules; - private List conditions; - - /** - * Run the matching function against the boolean stream. - * - * @param booleanStream Boolean stream from trait condition evaluations. - */ - public Boolean matchingFunction(Stream booleanStream) { - if (SegmentRules.ALL_RULE.getRule().equals(type)) { - return booleanStream.allMatch((bool) -> bool); - } else if (SegmentRules.ANY_RULE.getRule().equals(type)) { - return booleanStream.anyMatch((bool) -> bool); - } else if (SegmentRules.NONE_RULE.getRule().equals(type)) { - return !booleanStream.anyMatch((bool) -> bool); - } - - return false; - } -} diff --git a/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java b/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java deleted file mode 100644 index 683cf1ec..00000000 --- a/src/main/java/com/flagsmith/flagengine/segments/constants/SegmentRules.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.flagsmith.flagengine.segments.constants; - -public enum SegmentRules { - ALL_RULE("ALL"), ANY_RULE("ANY"), NONE_RULE("NONE"); - - private String rule; - - public String getRule() { - return rule; - } - - private SegmentRules(String rule) { - this.rule = rule; - } -} diff --git a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java index 3ea6d62e..9b05b2f3 100644 --- a/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java +++ b/src/main/java/com/flagsmith/flagengine/utils/types/TypeCasting.java @@ -32,7 +32,7 @@ public static Boolean compare(SegmentConditions condition, Object value1, Object return compare(condition, toSemver(value1), toSemver(value2)); } - return compare(condition, (String) value1, (String) value2); + return compare(condition, String.valueOf(value1), String.valueOf(value2)); } /** @@ -140,7 +140,7 @@ public static Boolean toBoolean(Object str) { return str instanceof Boolean ? ((Boolean) str) : BooleanUtils.toBoolean((String) str); } catch (Exception nfe) { - return null; + return false; } } @@ -151,8 +151,9 @@ public static Boolean toBoolean(Object str) { */ public static Boolean isBoolean(Object str) { return str instanceof Boolean - || Boolean.TRUE.toString().equalsIgnoreCase(((String) str)) - || Boolean.FALSE.toString().equalsIgnoreCase(((String) str)); + || Boolean.TRUE.toString().equalsIgnoreCase((String.valueOf(str))) + || Boolean.FALSE.toString().equalsIgnoreCase((String.valueOf(str))) + || "1".equals(String.valueOf(str)); } /** diff --git a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java index c3283d14..c8305786 100644 --- a/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java +++ b/src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java @@ -1,9 +1,9 @@ package com.flagsmith.interfaces; import com.flagsmith.config.FlagsmithConfig; -import com.flagsmith.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; +import com.flagsmith.flagengine.EvaluationContext; import com.flagsmith.models.Flags; +import com.flagsmith.models.TraitModel; import com.flagsmith.threads.RequestProcessor; import java.util.List; import okhttp3.HttpUrl; @@ -21,7 +21,7 @@ Flags identifyUserWithTraits( FlagsmithConfig getConfig(); - EnvironmentModel getEnvironment(); + EvaluationContext getEvaluationContext(); RequestProcessor getRequestor(); diff --git a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java index 48f6c1d9..ab574089 100644 --- a/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java +++ b/src/main/java/com/flagsmith/interfaces/IOfflineHandler.java @@ -1,6 +1,6 @@ package com.flagsmith.interfaces; -import com.flagsmith.flagengine.environments.EnvironmentModel; +import com.flagsmith.models.environments.EnvironmentModel; public interface IOfflineHandler { EnvironmentModel getEnvironment(); diff --git a/src/main/java/com/flagsmith/mappers/EngineMappers.java b/src/main/java/com/flagsmith/mappers/EngineMappers.java new file mode 100644 index 00000000..26087c31 --- /dev/null +++ b/src/main/java/com/flagsmith/mappers/EngineMappers.java @@ -0,0 +1,463 @@ +package com.flagsmith.mappers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.flagsmith.MapperFactory; +import com.flagsmith.flagengine.EngineConstants; +import com.flagsmith.flagengine.EnvironmentContext; +import com.flagsmith.flagengine.EvaluationContext; +import com.flagsmith.flagengine.FeatureContext; +import com.flagsmith.flagengine.FeatureValue; +import com.flagsmith.flagengine.FlagResult; +import com.flagsmith.flagengine.IdentityContext; +import com.flagsmith.flagengine.SegmentCondition; +import com.flagsmith.flagengine.SegmentContext; +import com.flagsmith.flagengine.SegmentRule; +import com.flagsmith.flagengine.Segments; +import com.flagsmith.flagengine.Traits; +import com.flagsmith.flagengine.segments.constants.SegmentConditions; +import com.flagsmith.models.FeatureMetadata; +import com.flagsmith.models.Flag; +import com.flagsmith.models.SegmentMetadata; +import com.flagsmith.models.environments.EnvironmentModel; +import com.flagsmith.models.features.FeatureModel; +import com.flagsmith.models.features.FeatureSegmentModel; +import com.flagsmith.models.features.FeatureStateModel; +import com.flagsmith.models.features.MultivariateFeatureStateValueModel; +import com.flagsmith.models.identities.IdentityModel; +import com.flagsmith.models.projects.ProjectModel; +import com.flagsmith.models.segments.SegmentConditionModel; +import com.flagsmith.models.segments.SegmentModel; +import com.flagsmith.models.segments.SegmentRuleModel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * EngineMappers + * + *

Utility class for mapping JSON data to flag engine context objects. + */ +public class EngineMappers { + /** + * Maps FlagResult to Flag. + * Returns null if metadata is missing or invalid. + * + * @param flagResult the flag result + * @return the mapped flag or null + */ + public static Flag mapFlagResultToFlag( + FlagResult flagResult + ) { + FeatureMetadata metadata; + + metadata = MapperFactory.getMapper() + .convertValue(flagResult.getMetadata(), FeatureMetadata.class); + + if (metadata == null || metadata.getFlagsmithId() == null) { + return null; + } + + Flag flag = new Flag(); + flag.setFeatureId(metadata.getFlagsmithId()); + flag.setFeatureName(flagResult.getName()); + flag.setValue(flagResult.getValue()); + flag.setEnabled(flagResult.getEnabled()); + return flag; + } + + /** + * Maps context and identity data to evaluation context. + * + * @param context the base evaluation context + * @param identifier the identity identifier + * @param traits optional traits mapping + * @return the updated evaluation context with identity information + */ + public static EvaluationContext mapContextAndIdentityDataToContext( + EvaluationContext context, + String identifier, + Map traits) { + + // Create identity context + IdentityContext identityContext = new IdentityContext() + .withIdentifier(identifier) + .withKey(context.getEnvironment().getKey() + "_" + identifier) + .withTraits(new Traits()); + + // Map traits if provided + if (traits != null && !traits.isEmpty()) { + for (Map.Entry entry : traits.entrySet()) { + Object traitValue = entry.getValue(); + // Handle TraitConfig-like objects (maps with "value" key) + if (traitValue instanceof Map) { + Map traitMap = (Map) traitValue; + if (traitMap.containsKey("value")) { + traitValue = traitMap.get("value"); + } + } + identityContext.getTraits().setAdditionalProperty(entry.getKey(), traitValue); + } + } + + // Create new evaluation context with identity + return new EvaluationContext(context) + .withIdentity(identityContext); + } + + /** + * Maps environment document to evaluation context. + * + * @param environmentDocument the environment document JSON + * @return the evaluation context + */ + public static EvaluationContext mapEnvironmentDocumentToContext( + JsonNode environmentDocument) { + return mapEnvironmentToContext( + MapperFactory.getMapper().convertValue(environmentDocument, + EnvironmentModel.class)); + } + + /** + * Maps environment model to evaluation context. + * + * @param environmentModel the environment model + * @return the evaluation context + */ + public static EvaluationContext mapEnvironmentToContext( + EnvironmentModel environmentModel) { + // Create environment context + final EnvironmentContext environmentContext = new EnvironmentContext() + .withKey(environmentModel.getApiKey()) + .withName(environmentModel.getName()); + + // Map features + Map features = new HashMap<>(); + for (FeatureStateModel featureState : environmentModel.getFeatureStates()) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + features.put(featureContext.getName(), featureContext); + } + + // Map segments + Map segments = new HashMap<>(); + + // Map project segments + ProjectModel project = environmentModel.getProject(); + for (SegmentModel segment : project.getSegments()) { + String segmentKey = String.valueOf(segment.getId()); + segments.put(segmentKey, mapSegmentToSegmentContext(segment)); + } + + // Map identity overrides + Map identityOverrideSegments = mapIdentityOverridesToSegments( + environmentModel.getIdentityOverrides()); + segments.putAll(identityOverrideSegments); + + // Create evaluation context + EvaluationContext evaluationContext = new EvaluationContext() + .withEnvironment(environmentContext); + + // Add features individually + com.flagsmith.flagengine.Features featuresObj = new com.flagsmith.flagengine.Features(); + for (Map.Entry entry : features.entrySet()) { + featuresObj.withAdditionalProperty(entry.getKey(), entry.getValue()); + } + evaluationContext.withFeatures(featuresObj); + + // Add segments individually + Segments segmentsObj = new Segments(); + for (Map.Entry entry : segments.entrySet()) { + segmentsObj.withAdditionalProperty(entry.getKey(), entry.getValue()); + } + evaluationContext.withSegments(segmentsObj); + + return evaluationContext; + } + + /** + * Maps identity overrides to segment contexts. + * + * @param identityOverrides the identity overrides JSON array + * @return map of segment contexts + */ + private static Map mapIdentityOverridesToSegments( + List identityOverrides) { + + // Map from sorted list of feature contexts to identifiers + Map, List> featuresToIdentifiers = new HashMap<>(); + + for (IdentityModel identityOverride : identityOverrides) { + List identityFeatures = identityOverride.getIdentityFeatures(); + if (identityFeatures == null || identityFeatures.isEmpty()) { + continue; + } + + // Create overrides key as a sorted list of FeatureContext objects + List overridesKey = new ArrayList<>(); + List sortedFeatures = new ArrayList<>(); + identityFeatures.forEach(sortedFeatures::add); + sortedFeatures.sort((a, b) -> a.getFeature().getName() + .compareTo(b.getFeature().getName())); + + for (FeatureStateModel featureState : sortedFeatures) { + FeatureModel feature = featureState.getFeature(); + FeatureContext featureContext = new FeatureContext() + .withKey("") + .withFeatureKey(String.valueOf(feature.getId())) + .withName(feature.getName()) + .withEnabled(featureState.getEnabled()) + .withValue(featureState.getValue()) + .withPriority(EngineConstants.STRONGEST_PRIORITY) + .withMetadata(mapFeatureStateToFeatureMetadata(featureState)); + overridesKey.add(featureContext); + } + + String identifier = identityOverride.getIdentifier(); + featuresToIdentifiers.computeIfAbsent(overridesKey, k -> new ArrayList<>()).add(identifier); + } + + Map segmentContexts = new HashMap<>(); + for (Map.Entry, List> entry : featuresToIdentifiers.entrySet()) { + List overridesKey = entry.getKey(); + List identifiers = entry.getValue(); + + String segmentKey = getVirtualSegmentKey(overridesKey); + + // Create segment condition for identifier check + SegmentCondition identifierCondition = new SegmentCondition() + .withProperty("$.identity.identifier") + .withOperator(SegmentConditions.IN) + .withValue(identifiers); + + // Create segment rule + SegmentRule segmentRule = new SegmentRule() + .withType(SegmentRule.Type.ALL) + .withConditions(List.of(identifierCondition)); + + // Create overrides from FeatureContext objects + List overrides = new ArrayList<>(); + for (FeatureContext featureContext : overridesKey) { + // Copy the feature context for the override + FeatureContext override = new FeatureContext(featureContext) + .withKey(""); // Identity overrides never carry multivariate options + overrides.add(override); + } + + SegmentMetadata metadata = new SegmentMetadata(); + metadata.setSource(SegmentMetadata.Source.IDENTITY_OVERRIDES); + + Map metadataMap = MapperFactory.getMapper() + .convertValue(metadata, + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + + SegmentContext segmentContext = new SegmentContext() + .withKey("") // Identity override segments never use % Split operator + .withName("identity_overrides") + .withRules(List.of(segmentRule)) + .withOverrides(overrides) + .withMetadata(metadataMap); + + segmentContexts.put(segmentKey, segmentContext); + } + + return segmentContexts; + } + + /** + * Maps environment document rules to context rules. + * + * @param rules the rules JSON array + * @return list of segment rules + */ + private static List mapEnvironmentDocumentRulesToContextRules( + List rules) { + + List segmentRules = new ArrayList<>(); + + for (SegmentRuleModel rule : rules) { + // Map conditions + List conditions = new ArrayList<>(); + + if (rule.getConditions() != null) { + for (SegmentConditionModel condition : rule.getConditions()) { + SegmentCondition segmentCondition = new SegmentCondition() + .withProperty(condition.getProperty()) + .withOperator(condition.getOperator()) + .withValue(condition.getValue()); + conditions.add(segmentCondition); + } + } + + // Map sub-rules recursively + List subRules = mapEnvironmentDocumentRulesToContextRules( + rule.getRules()); + + SegmentRule segmentRule = new SegmentRule() + .withType(SegmentRule.Type.fromValue(rule.getType())) + .withConditions(conditions) + .withRules(subRules); + + segmentRules.add(segmentRule); + } + + return segmentRules; + } + + /** + * Maps environment document feature states to feature contexts. + * + * @param featureStates the feature states JSON array + * @return list of feature contexts + */ + private static List mapEnvironmentDocumentFeatureStatesToFeatureContexts( + List featureStates) { + + List featureContexts = new ArrayList<>(); + + if (featureStates != null) { + for (FeatureStateModel featureState : featureStates) { + FeatureContext featureContext = mapFeatureStateToFeatureContext(featureState); + featureContexts.add(featureContext); + } + } + + return featureContexts; + } + + /** + * Gets the feature state key from either django_id or featurestate_uuid. + * + * @param featureState the feature state JSON + * @return the feature state key as string + */ + private static String getFeatureStateKey(FeatureStateModel featureState) { + Integer djangoId = featureState.getDjangoId(); + if (djangoId != null) { + return djangoId.toString(); + } + return featureState.getFeaturestateUuid(); + } + + private static double getMultivariateFeatureValuePriority( + MultivariateFeatureStateValueModel multivariateValue) { + if (multivariateValue.getId() != null) { + return multivariateValue.getId(); + } + + // Fallback to mv_fs_value_uuid if id is not present + UUID mvFsValueUuid = UUID.fromString(multivariateValue.getMvFsValueUuid()); + return mvFsValueUuid.getMostSignificantBits() & Long.MAX_VALUE; + } + + /** + * Maps a single feature state to feature context. + * + * @param featureState the feature state JSON + * @return the feature context + */ + private static FeatureContext mapFeatureStateToFeatureContext(FeatureStateModel featureState) { + FeatureContext featureContext = new FeatureContext() + .withKey(getFeatureStateKey(featureState)) + .withFeatureKey(String.valueOf(featureState.getFeature().getId())) + .withName(featureState.getFeature().getName()) + .withEnabled(featureState.getEnabled()) + .withValue(featureState.getValue()) + .withMetadata(mapFeatureStateToFeatureMetadata(featureState)); + + // Handle multivariate feature state values + 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 + FeatureSegmentModel featureSegment = featureState.getFeatureSegment(); + if (featureSegment != null) { + Double priority = (double) featureSegment.getPriority(); + if (priority != null) { + featureContext.withPriority(priority); + } + } + + 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. + * + * @param segment the segment JSON + * @return the segment context + */ + private static SegmentContext mapSegmentToSegmentContext(SegmentModel segment) { + // Map rules + List rules = mapEnvironmentDocumentRulesToContextRules( + segment.getRules()); + + // Map overrides + List segmentFeatureStates = segment.getFeatureStates(); + List overrides = mapEnvironmentDocumentFeatureStatesToFeatureContexts( + segmentFeatureStates); + + // Map metadata + SegmentMetadata metadata = new SegmentMetadata(); + metadata.setSource(SegmentMetadata.Source.API); + metadata.setFlagsmithId(segment.getId()); + + Map metadataMap = MapperFactory.getMapper() + .convertValue(metadata, + new com.fasterxml.jackson.core.type.TypeReference>() { + }); + + String segmentKey = String.valueOf(segment.getId()); + return new SegmentContext() + .withKey(segmentKey) + .withName(segment.getName()) + .withRules(rules) + .withOverrides(overrides) + .withMetadata(metadataMap); + } + + /** + * Generates a unique segment key based on feature contexts. + * Uses a combination of feature names and values to ensure + * uniqueness. + * + * @param featureContexts list of feature contexts + * @return unique segment key + */ + private static String getVirtualSegmentKey( + List 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 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/Flag.java b/src/main/java/com/flagsmith/models/Flag.java index 1a24e772..990976b9 100644 --- a/src/main/java/com/flagsmith/models/Flag.java +++ b/src/main/java/com/flagsmith/models/Flag.java @@ -1,9 +1,8 @@ package com.flagsmith.models; import com.fasterxml.jackson.databind.JsonNode; -import com.flagsmith.flagengine.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureStateModel; import lombok.Data; -import lombok.ToString; @Data public class Flag extends BaseFlag { @@ -14,13 +13,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 4961d22e..f09792bd 100644 --- a/src/main/java/com/flagsmith/models/Flags.java +++ b/src/main/java/com/flagsmith/models/Flags.java @@ -4,12 +4,16 @@ 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.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; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.stream.Collectors; import lombok.Data; @@ -22,48 +26,32 @@ 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( List featureStates, AnalyticsProcessor analyticsProcessor) { - return fromFeatureStateModels(featureStates, analyticsProcessor, null, null); + return fromFeatureStateModels(featureStates, analyticsProcessor, null); } /** * 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) - */ - 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); @@ -76,7 +64,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 +78,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 +92,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 +106,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(); @@ -131,6 +117,33 @@ 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 = 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); + 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/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/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java b/src/main/java/com/flagsmith/models/TraitModel.java similarity index 88% rename from src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java rename to src/main/java/com/flagsmith/models/TraitModel.java index 5864dea2..9d287710 100644 --- a/src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java +++ b/src/main/java/com/flagsmith/models/TraitModel.java @@ -1,4 +1,4 @@ -package com.flagsmith.flagengine.identities.traits; +package com.flagsmith.models; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java b/src/main/java/com/flagsmith/models/environments/EnvironmentModel.java similarity index 65% rename from src/main/java/com/flagsmith/flagengine/environments/EnvironmentModel.java rename to src/main/java/com/flagsmith/models/environments/EnvironmentModel.java index 58f1ac3b..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; @@ -14,6 +14,10 @@ public class EnvironmentModel extends BaseModel { @JsonProperty("api_key") private String apiKey; + + @JsonProperty("name") + private String name; + private ProjectModel project; @JsonProperty("feature_states") @@ -21,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/models/features/FeatureModel.java b/src/main/java/com/flagsmith/models/features/FeatureModel.java new file mode 100644 index 00000000..761371c3 --- /dev/null +++ b/src/main/java/com/flagsmith/models/features/FeatureModel.java @@ -0,0 +1,10 @@ +package com.flagsmith.models.features; + +import lombok.Data; + +@Data +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/models/features/FeatureSegmentModel.java b/src/main/java/com/flagsmith/models/features/FeatureSegmentModel.java new file mode 100644 index 00000000..619b639f --- /dev/null +++ b/src/main/java/com/flagsmith/models/features/FeatureSegmentModel.java @@ -0,0 +1,9 @@ +package com.flagsmith.models.features; + +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/models/features/FeatureStateModel.java b/src/main/java/com/flagsmith/models/features/FeatureStateModel.java new file mode 100644 index 00000000..ccaef00f --- /dev/null +++ b/src/main/java/com/flagsmith/models/features/FeatureStateModel.java @@ -0,0 +1,23 @@ +package com.flagsmith.models.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; +} \ 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 70% rename from src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureOptionModel.java rename to src/main/java/com/flagsmith/models/features/MultivariateFeatureOptionModel.java index 6d1c6260..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,5 +6,4 @@ @Data public class MultivariateFeatureOptionModel extends BaseModel { private String value; - private Integer id; -} +} \ 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 80% rename from src/main/java/com/flagsmith/flagengine/features/MultivariateFeatureStateValueModel.java rename to src/main/java/com/flagsmith/models/features/MultivariateFeatureStateValueModel.java index 6e197b5a..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,8 +14,4 @@ public class MultivariateFeatureStateValueModel extends BaseModel { private Integer id; @JsonProperty("mv_fs_value_uuid") private String mvFsValueUuid = UUID.randomUUID().toString(); - - public Comparable getSortValue() { - return id != null ? id : mvFsValueUuid; - } -} +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/models/identities/IdentityModel.java b/src/main/java/com/flagsmith/models/identities/IdentityModel.java new file mode 100644 index 00000000..7d56f912 --- /dev/null +++ b/src/main/java/com/flagsmith/models/identities/IdentityModel.java @@ -0,0 +1,23 @@ +package com.flagsmith.models.identities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.flagsmith.models.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<>(); +} \ No newline at end of file diff --git a/src/main/java/com/flagsmith/models/projects/ProjectModel.java b/src/main/java/com/flagsmith/models/projects/ProjectModel.java new file mode 100644 index 00000000..8e77f5b1 --- /dev/null +++ b/src/main/java/com/flagsmith/models/projects/ProjectModel.java @@ -0,0 +1,11 @@ +package com.flagsmith.models.projects; + +import com.flagsmith.models.segments.SegmentModel; +import com.flagsmith.utils.models.BaseModel; +import java.util.List; +import lombok.Data; + +@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 56% rename from src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java rename to src/main/java/com/flagsmith/models/segments/SegmentConditionModel.java index e8956b34..e4fb910a 100644 --- a/src/main/java/com/flagsmith/flagengine/segments/SegmentConditionModel.java +++ b/src/main/java/com/flagsmith/models/segments/SegmentConditionModel.java @@ -1,5 +1,6 @@ -package com.flagsmith.flagengine.segments; +package com.flagsmith.models.segments; +import com.fasterxml.jackson.annotation.JsonProperty; import com.flagsmith.flagengine.segments.constants.SegmentConditions; import lombok.Data; @@ -7,7 +8,6 @@ public class SegmentConditionModel { private SegmentConditions operator; private String value; - //CHECKSTYLE:OFF - private String property_; - //CHECKSTYLE:ON -} + @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 dc13c2f9..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,5 +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/models/segments/SegmentRuleModel.java b/src/main/java/com/flagsmith/models/segments/SegmentRuleModel.java new file mode 100644 index 00000000..5e904f2c --- /dev/null +++ b/src/main/java/com/flagsmith/models/segments/SegmentRuleModel.java @@ -0,0 +1,11 @@ +package com.flagsmith.models.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/offline/LocalFileHandler.java b/src/main/java/com/flagsmith/offline/LocalFileHandler.java index 562f1b0b..c604ed3c 100644 --- a/src/main/java/com/flagsmith/offline/LocalFileHandler.java +++ b/src/main/java/com/flagsmith/offline/LocalFileHandler.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.flagsmith.MapperFactory; import com.flagsmith.exceptions.FlagsmithClientError; -import com.flagsmith.flagengine.environments.EnvironmentModel; import com.flagsmith.interfaces.IOfflineHandler; +import com.flagsmith.models.environments.EnvironmentModel; import java.io.File; import java.io.IOException; diff --git a/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java b/src/main/java/com/flagsmith/responses/FlagsAndTraitsResponse.java index 12cffca5..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.flagengine.features.FeatureStateModel; +import com.flagsmith.models.features.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..253a2538 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; 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 diff --git a/src/test/java/com/flagsmith/DummyOfflineHandler.java b/src/test/java/com/flagsmith/DummyOfflineHandler.java index f3fdbaeb..ff3eea8e 100644 --- a/src/test/java/com/flagsmith/DummyOfflineHandler.java +++ b/src/test/java/com/flagsmith/DummyOfflineHandler.java @@ -1,7 +1,7 @@ package com.flagsmith; -import com.flagsmith.flagengine.environments.EnvironmentModel; import com.flagsmith.interfaces.IOfflineHandler; +import com.flagsmith.models.environments.EnvironmentModel; public class DummyOfflineHandler implements IOfflineHandler { public EnvironmentModel getEnvironment() { diff --git a/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java b/src/test/java/com/flagsmith/FlagsmithApiWrapperCachingTest.java index c99fd1bb..40320683 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.features.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..7f6deb07 100644 --- a/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java +++ b/src/test/java/com/flagsmith/FlagsmithApiWrapperTest.java @@ -18,12 +18,13 @@ 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.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureModel; 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; @@ -193,10 +194,10 @@ public void testClose_ClosesAnalyticsProcessor() { } private FeatureStateModel getNewFlag() { + final FeatureStateModel flag = new FeatureStateModel(); final FeatureModel feature = new FeatureModel(); feature.setName("my-test-flag"); feature.setId(123); - final FeatureStateModel flag = new FeatureStateModel(); flag.setFeature(feature); return flag; diff --git a/src/test/java/com/flagsmith/FlagsmithClientTest.java b/src/test/java/com/flagsmith/FlagsmithClientTest.java index 98940c74..3efe94bf 100644 --- a/src/test/java/com/flagsmith/FlagsmithClientTest.java +++ b/src/test/java/com/flagsmith/FlagsmithClientTest.java @@ -7,7 +7,6 @@ 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; @@ -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.interfaces.FlagsmithCache; import com.flagsmith.models.BaseFlag; import com.flagsmith.models.DefaultFlag; +import com.flagsmith.models.environments.EnvironmentModel; +import com.flagsmith.models.features.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; @@ -159,18 +158,18 @@ public void testClient_validateEnvironment() .setApiKey("api-key") .build(); - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); interceptor.addRule() .get(baseUrl + "/environment-document/") .anyTimes() .respond( - MapperFactory.getMapper().writeValueAsString(environmentModel), + FlagsmithTestHelper.environmentString(), MEDIATYPE_JSON); client.updateEnvironment(); - assertNotNull(client.getEnvironment()); - assertEquals(client.getEnvironment(), environmentModel); + assertNotNull(client.getEvaluationContext()); + assertEquals(client.getEvaluationContext(), evaluationContext); } @Test @@ -548,11 +547,11 @@ public void testGetIdentitySegmentsWithValidTrait() throws JsonProcessingExcepti @Test public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionAndEnvironmentExists() { // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()) - .thenReturn(environmentModel) + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) .thenThrow(RuntimeException.class); FlagsmithClient client = FlagsmithClient.newBuilder() @@ -570,17 +569,17 @@ public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentThrowsExceptionA // Then // No exception is thrown and the client environment remains what was first // retrieved from the ApiWrapper - assertEquals(client.getEnvironment(), environmentModel); + assertEquals(client.getEvaluationContext(), evaluationContext); } @Test public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentExists() { // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()) - .thenReturn(environmentModel) + when(mockApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) .thenReturn(null); FlagsmithClient client = FlagsmithClient.newBuilder() @@ -597,14 +596,14 @@ public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEn // Then // The client environment is not overwritten with null - assertEquals(client.getEnvironment(), environmentModel); + assertEquals(client.getEvaluationContext(), evaluationContext); } @Test public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEnvironmentNotExists() { // Given FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); + when(mockApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); FlagsmithClient client = FlagsmithClient.newBuilder() .withFlagsmithApiWrapper(mockApiWrapper) @@ -617,20 +616,26 @@ public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEn // Then // The environment remains null - assertEquals(client.getEnvironment(), null); + assertEquals(client.getEvaluationContext(), null); } @Test - public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() { + public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() + throws FlagsmithClientError { // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); + + FlagsmithConfig config = FlagsmithConfig.newBuilder() + .withLocalEvaluation(true) + .build(); FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockApiWrapper.getEnvironment()).thenReturn(environmentModel); + 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(); @@ -639,10 +644,10 @@ public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentRetu // 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"); + assertEquals( + client.getIdentityFlags("overridden-identity") + .getFlag("some_feature").getValue(), + "overridden-value"); } @Test @@ -686,13 +691,13 @@ public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClien // evaluate flags soon after the client is instantiated. // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()) - .thenReturn(environmentModel) + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) .thenReturn(null); when(mockedApiWrapper.getConfig()).thenReturn(config); @@ -723,13 +728,13 @@ public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClien @Test public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError { // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()) - .thenReturn(environmentModel) + when(mockedApiWrapper.getEvaluationContext()) + .thenReturn(evaluationContext) .thenReturn(null); when(mockedApiWrapper.getConfig()).thenReturn(config); @@ -755,7 +760,7 @@ public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmen // Given FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); when(mockedApiWrapper.getConfig()).thenReturn(config); FlagsmithClient client = FlagsmithClient.newBuilder() @@ -778,7 +783,7 @@ public void testGetIdentityFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNu // Given FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build(); FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenThrow(RuntimeException.class); + when(mockedApiWrapper.getEvaluationContext()).thenThrow(RuntimeException.class); when(mockedApiWrapper.getConfig()).thenReturn(config); FlagsmithClient client = FlagsmithClient.newBuilder() @@ -808,7 +813,7 @@ public void testClose() throws FlagsmithApiError, InterruptedException { .build(); FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class); - when(mockedApiWrapper.getEnvironment()).thenReturn(FlagsmithTestHelper.environmentModel()); + when(mockedApiWrapper.getEvaluationContext()).thenReturn(FlagsmithTestHelper.evaluationContext()); when(mockedApiWrapper.getConfig()).thenReturn(config); FlagsmithClient client = FlagsmithClient.newBuilder() @@ -831,7 +836,7 @@ public void testClose() throws FlagsmithApiError, InterruptedException { @Test public void testOfflineMode() throws FlagsmithClientError { // Given - EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel(); + EvaluationContext evaluationContext = FlagsmithTestHelper.evaluationContext(); FlagsmithConfig config = FlagsmithConfig .newBuilder() .withOfflineMode(true) @@ -842,7 +847,7 @@ public void testOfflineMode() throws FlagsmithClientError { FlagsmithClient client = FlagsmithClient.newBuilder().withConfiguration(config).build(); // Then - assertEquals(environmentModel, client.getEnvironment()); + assertEquals(evaluationContext, client.getEvaluationContext()); Flags environmentFlags = client.getEnvironmentFlags(); assertTrue(environmentFlags.isFeatureEnabled("some_feature")); diff --git a/src/test/java/com/flagsmith/FlagsmithTestHelper.java b/src/test/java/com/flagsmith/FlagsmithTestHelper.java index bc606879..ae1f5203 100644 --- a/src/test/java/com/flagsmith/FlagsmithTestHelper.java +++ b/src/test/java/com/flagsmith/FlagsmithTestHelper.java @@ -7,13 +7,14 @@ 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.features.FeatureStateModel; +import com.flagsmith.models.features.FeatureModel; 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; @@ -241,15 +242,15 @@ public static int createProject(String name, int organisationId) { public static BaseFlag flag( String name, String description, String type, boolean enabled, String value ) { + final FeatureStateModel result = new FeatureStateModel(); + result.setEnabled(enabled); + result.setValue(value); + final FeatureModel feature = new FeatureModel(); feature.setName(name); feature.setType(type); - final FeatureStateModel result = new FeatureStateModel(); - result.setFeature(feature); - result.setEnabled(enabled); - result.setValue(value); - return Flag.fromFeatureStateModel(result, null); + return Flag.fromFeatureStateModel(result); } public static BaseFlag flag(String name, String description, boolean enabled) { @@ -267,40 +268,10 @@ 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" + + " \"name\": \"Test Environment\",\n" + " \"project\": {\n" + " \"name\": \"Test project\",\n" + " \"organisation\": {\n" + @@ -381,12 +352,18 @@ public static String environmentString() { public static EnvironmentModel environmentModel() { try { - return EnvironmentModel.load(MapperFactory.getMapper().readTree(environmentString()), EnvironmentModel.class); + return MapperFactory.getMapper().readValue(environmentString(), EnvironmentModel.class); } catch (JsonProcessingException e) { - // environment model json + throw new RuntimeException("Failed to parse environment JSON", e); } + } - return null; + public static EvaluationContext evaluationContext() { + try { + return EngineMappers.mapEnvironmentDocumentToContext(MapperFactory.getMapper().readTree(environmentString())); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse environment JSON", e); + } } public static List getFlags() { @@ -476,7 +453,7 @@ public static JsonNode getIdentityRequest( 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..547c6012 100644 --- a/src/test/java/com/flagsmith/flagengine/EngineTest.java +++ b/src/test/java/com/flagsmith/flagengine/EngineTest.java @@ -1,94 +1,59 @@ package com.flagsmith.flagengine; +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.flagengine.environments.EnvironmentModel; -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.IdentityModel; -import com.flagsmith.flagengine.models.ResponseJSON; -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.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -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 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 identitiesAndResponses = engineTestData.get("identities_and_responses"); - - 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); - - returnValues.add(Arguments.of(identityModel, environmentModel, expectedResponse)); - - } - } - - return returnValues.stream(); - - } catch (Exception e) { - System.out.println(e.getMessage()); + 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.argumentSet( + path.getFileName().toString(), + 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(IdentityModel identity, EnvironmentModel environmentModel, ResponseJSON expectedResponse) { - List featureStates = - Engine.getIdentityFeatureStates(environmentModel, identity); + public void testEngine(EvaluationContext evaluationContext, EvaluationResult expectedResult) { + EvaluationResult evaluationResult = Engine.getEvaluationResult(evaluationContext); - List sortedFeatureStates = featureStates - .stream() - .sorted((featureState1, t1) - -> featureState1.getFeature().getName() - .compareTo(t1.getFeature().getName())) - .collect(Collectors.toList()); - - List sortedResponse = expectedResponse.getFlags() - .stream() - .sorted((featureState1, t1) - -> featureState1.getFeature().getName() - .compareTo(t1.getFeature().getName())) - .collect(Collectors.toList()); - - assert (sortedResponse.size() == sortedFeatureStates.size()); - - int index = 0; - for (FeatureStateModel featureState : sortedFeatureStates) { - Object featureStateValue = featureState.getValue(identity.getDjangoId()); - Object expectedResponseValue = sortedResponse.get(index).getValue(identity.getDjangoId()); - - assertEquals(featureStateValue, expectedResponseValue); - assertEquals(featureState.getEnabled(), sortedResponse.get(index).getEnabled()); - index++; - } + assertThat(evaluationResult) + .usingRecursiveComparison(recursiveComparisonConfig) + .ignoringAllOverriddenEquals() + .isEqualTo(expectedResult); } } diff --git a/src/test/java/com/flagsmith/flagengine/enginetestdata b/src/test/java/com/flagsmith/flagengine/enginetestdata index 71a96319..41c20214 160000 --- a/src/test/java/com/flagsmith/flagengine/enginetestdata +++ b/src/test/java/com/flagsmith/flagengine/enginetestdata @@ -1 +1 @@ -Subproject commit 71a963198d66d681d12f2bf92c42a3036ffe92a7 +Subproject commit 41c202145e375c712600e318c439456de5b221d7 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/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()); + } +} 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 3001d962..00000000 --- a/src/test/java/com/flagsmith/flagengine/models/ResponseJSON.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.flagsmith.flagengine.models; - -import com.flagsmith.flagengine.features.FeatureStateModel; -import com.flagsmith.flagengine.identities.traits.TraitModel; -import lombok.Data; - -import java.util.List; - -@Data -public class ResponseJSON { - private List flags; - private List traits; -} 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..af889785 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,146 +21,136 @@ 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(); + public static SegmentContext segmentMultipleConditionsAll() { + SegmentCondition segmentCondition = new SegmentCondition(); segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); + segmentCondition.setProperty(traitKey1); segmentCondition.setValue(traitValue1); - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); + SegmentCondition segmentCondition2 = new SegmentCondition(); segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); + segmentCondition2.setProperty(traitKey2); segmentCondition2.setValue(traitValue2); - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ALL_RULE.getRule()); + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ALL); segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - SegmentModel segment = new SegmentModel(); - segment.setId(3); + SegmentContext segment = new SegmentContext(); + segment.setKey("3"); segment.setName("segment_multiple_conditions_all"); segment.setRules(Arrays.asList(segmentRule)); return segment; } - public static SegmentModel segmentMultipleConditionsAny() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); + public static SegmentContext segmentMultipleConditionsAny() { + SegmentCondition segmentCondition = new SegmentCondition(); segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); + segmentCondition.setProperty(traitKey1); segmentCondition.setValue(traitValue1); - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); + SegmentCondition segmentCondition2 = new SegmentCondition(); segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); + segmentCondition2.setProperty(traitKey2); segmentCondition2.setValue(traitValue2); - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - SegmentModel segment = new SegmentModel(); - segment.setId(4); + SegmentContext segment = new SegmentContext(); + segment.setKey("4"); segment.setName("segment_multiple_conditions_any"); segment.setRules(Arrays.asList(segmentRule)); return segment; } - public static SegmentModel segmentNestedRules() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); + public static SegmentContext segmentNestedRules() { + SegmentCondition segmentCondition = new SegmentCondition(); segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); + segmentCondition.setProperty(traitKey1); segmentCondition.setValue(traitValue1); - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); + SegmentCondition segmentCondition2 = new SegmentCondition(); segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); + segmentCondition2.setProperty(traitKey2); segmentCondition2.setValue(traitValue2); - SegmentConditionModel segmentCondition3 = new SegmentConditionModel(); + SegmentCondition segmentCondition3 = new SegmentCondition(); segmentCondition3.setOperator(SegmentConditions.EQUAL); - segmentCondition3.setProperty_(traitKey3); + segmentCondition3.setProperty(traitKey3); segmentCondition3.setValue(traitValue3); - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); segmentRule.setConditions(Arrays.asList(segmentCondition, segmentCondition2)); - SegmentRuleModel segmentRule2 = new SegmentRuleModel(); - segmentRule2.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule2 = new SegmentRule(); + segmentRule2.setType(SegmentRule.Type.ANY); segmentRule2.setConditions(Arrays.asList(segmentCondition3)); - SegmentRuleModel segmentRule3 = new SegmentRuleModel(); - segmentRule3.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule3 = new SegmentRule(); + segmentRule3.setType(SegmentRule.Type.ANY); segmentRule3.setRules(Arrays.asList(segmentRule, segmentRule2)); - SegmentModel segment = new SegmentModel(); - segment.setId(5); + SegmentContext segment = new SegmentContext(); + segment.setKey("5"); segment.setName("segment_nested_rules_all"); segment.setRules(Arrays.asList(segmentRule3)); return segment; } - public static SegmentModel segmentConditionsAndNestedRules() { - SegmentConditionModel segmentCondition = new SegmentConditionModel(); + public static SegmentContext segmentConditionsAndNestedRules() { + SegmentCondition segmentCondition = new SegmentCondition(); segmentCondition.setOperator(SegmentConditions.EQUAL); - segmentCondition.setProperty_(traitKey1); + segmentCondition.setProperty(traitKey1); segmentCondition.setValue(traitValue1); - SegmentConditionModel segmentCondition2 = new SegmentConditionModel(); + SegmentCondition segmentCondition2 = new SegmentCondition(); segmentCondition2.setOperator(SegmentConditions.EQUAL); - segmentCondition2.setProperty_(traitKey2); + segmentCondition2.setProperty(traitKey2); segmentCondition2.setValue(traitValue2); - SegmentConditionModel segmentCondition3 = new SegmentConditionModel(); + SegmentCondition segmentCondition3 = new SegmentCondition(); segmentCondition3.setOperator(SegmentConditions.EQUAL); - segmentCondition3.setProperty_(traitKey3); + segmentCondition3.setProperty(traitKey3); segmentCondition3.setValue(traitValue3); - SegmentRuleModel segmentRule = new SegmentRuleModel(); - segmentRule.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule = new SegmentRule(); + segmentRule.setType(SegmentRule.Type.ANY); segmentRule.setConditions(Arrays.asList(segmentCondition)); - SegmentRuleModel segmentRule2 = new SegmentRuleModel(); - segmentRule2.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule2 = new SegmentRule(); + segmentRule2.setType(SegmentRule.Type.ANY); segmentRule2.setConditions(Arrays.asList(segmentCondition2)); - SegmentRuleModel segmentRule3 = new SegmentRuleModel(); - segmentRule3.setType(SegmentRules.ANY_RULE.getRule()); + SegmentRule segmentRule3 = new SegmentRule(); + segmentRule3.setType(SegmentRule.Type.ANY); segmentRule3.setConditions(Arrays.asList(segmentCondition3)); segmentRule.setRules(Arrays.asList(segmentRule2, segmentRule3)); - SegmentModel segment = new SegmentModel(); - segment.setId(6); + SegmentContext segment = new SegmentContext(); + segment.setKey("6"); segment.setName("segment_multiple_conditions_all_and_nested_rules"); segment.setRules(Arrays.asList(segmentRule3)); 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..f7b23fd3 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentEvaluatorTest.java @@ -1,15 +1,16 @@ 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.*; +import com.flagsmith.FlagsmithTestHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; @@ -47,16 +48,18 @@ private static Stream identitiesInSegments() { @ParameterizedTest @MethodSource("identitiesInSegments") - public void testIdentityInSegment(SegmentModel segment, List identityTraits, + public void testContextInSegment(SegmentContext segment, List identityTraits, Boolean expectedResponse) { - IdentityModel mockIdentity = new IdentityModel(); - mockIdentity.setIdentifier("foo"); - mockIdentity.setIdentityTraits(identityTraits); - mockIdentity.setEnvironmentApiKey("api-key"); - Boolean actualResult = SegmentEvaluator.evaluateIdentityInSegment(mockIdentity, segment, null); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + FlagsmithTestHelper.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)); + Assertions.assertEquals(expectedResponse, actualResult); } private static Stream traitExistenceChecks() { @@ -76,71 +79,25 @@ public void testTraitExistenceConditions(SegmentConditions conditionOperator, St 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"); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + FlagsmithTestHelper.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 - 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))); + 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..ce0da7c6 100644 --- a/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java +++ b/src/test/java/com/flagsmith/flagengine/unit/segments/SegmentModelTest.java @@ -1,15 +1,24 @@ 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; +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; import org.junit.jupiter.params.provider.MethodSource; +import java.util.Arrays; +import java.util.Collections; import java.util.stream.Stream; public class SegmentModelTest { @@ -86,9 +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) ); } @@ -98,17 +111,23 @@ private static Stream conditionTestData() { public void testSegmentConditionMatchesTraitValue( SegmentConditions condition, Object traitValue, - String conditionValue, + Object conditionValue, Boolean expectedResponse) { - SegmentConditionModel conditionModel = new SegmentConditionModel(); - conditionModel.setValue(conditionValue); - conditionModel.setOperator(condition); - conditionModel.setProperty_("foo"); + final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext( + FlagsmithTestHelper.evaluationContext(), "foo", + Collections.singletonMap("foo", traitValue)); + + 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.conditionMatchesTraitValue(conditionModel, traitValue); + Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segmentContext); - assertTrue(actualResult.equals(expectedResponse)); + assertEquals(expectedResponse, actualResult); } @ParameterizedTest @@ -119,14 +138,19 @@ public void testSemverMatchesTraitValue( String conditionValue, Boolean expectedResponse) { - SegmentConditionModel conditionModel = new SegmentConditionModel(); - conditionModel.setValue(conditionValue); - conditionModel.setOperator(condition); - conditionModel.setProperty_("foo"); + 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))))); - Boolean actualResult = SegmentEvaluator.conditionMatchesTraitValue(conditionModel, traitValue); + Boolean actualResult = SegmentEvaluator.isContextInSegment(context, segmentContext); - assertTrue(actualResult.equals(expectedResponse)); + assertEquals(expectedResponse, actualResult); } private static Stream semverTestData() { 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;