diff --git a/core-api/src/main/java/com/optimizely/ab/config/Experiment.java b/core-api/src/main/java/com/optimizely/ab/config/Experiment.java index e77becd39..11530735c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Experiment.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Experiment.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019, Optimizely and contributors + * Copyright 2016-2019, 2021, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ import com.fasterxml.jackson.annotation.*; import com.optimizely.ab.annotations.VisibleForTesting; -import com.optimizely.ab.config.audience.AudienceIdCondition; -import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.*; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -43,6 +42,10 @@ public class Experiment implements IdKeyMapped { private final String layerId; private final String groupId; + private final String AND = "AND"; + private final String OR = "OR"; + private final String NOT = "NOT"; + private final List audienceIds; private final Condition audienceConditions; private final List variations; @@ -173,6 +176,98 @@ public boolean isLaunched() { return status.equals(ExperimentStatus.LAUNCHED.toString()); } + public String serializeConditions(Map audiencesMap) { + Condition condition = this.audienceConditions; + return condition instanceof EmptyCondition ? "" : this.serialize(condition, audiencesMap); + } + + private String getNameFromAudienceId(String audienceId, Map audiencesMap) { + StringBuilder audienceName = new StringBuilder(); + if (audiencesMap != null && audiencesMap.get(audienceId) != null) { + audienceName.append("\"" + audiencesMap.get(audienceId) + "\""); + } else { + audienceName.append("\"" + audienceId + "\""); + } + return audienceName.toString(); + } + + private String getOperandOrAudienceId(Condition condition, Map audiencesMap) { + if (condition != null) { + if (condition instanceof AudienceIdCondition) { + return this.getNameFromAudienceId(condition.getOperandOrId(), audiencesMap); + } else { + return condition.getOperandOrId(); + } + } else { + return ""; + } + } + + public String serialize(Condition condition, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + List conditions; + + String operand = this.getOperandOrAudienceId(condition, audiencesMap); + switch (operand){ + case (AND): + conditions = ((AndCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (OR): + conditions = ((OrCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (NOT): + stringBuilder.append(operand + " "); + Condition notCondition = ((NotCondition) condition).getCondition(); + if (notCondition instanceof AudienceIdCondition) { + stringBuilder.append(serialize(notCondition, audiencesMap)); + } else { + stringBuilder.append("(" + serialize(notCondition, audiencesMap) + ")"); + } + break; + default: + stringBuilder.append(operand); + break; + } + + return stringBuilder.toString(); + } + + public String getNameOrNextCondition(String operand, List conditions, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + int index = 0; + if (conditions.isEmpty()) { + return ""; + } else if (conditions.size() == 1) { + return serialize(conditions.get(0), audiencesMap); + } else { + for (Condition con : conditions) { + index++; + if (index + 1 <= conditions.size()) { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append( audienceName + " "); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ") "); + } + stringBuilder.append(operand); + stringBuilder.append(" "); + } else { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append(audienceName); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ")"); + } + } + } + } + return stringBuilder.toString(); + } + @Override public String toString() { return "Experiment{" + diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java index 8b458d059..dca3cefc8 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java @@ -23,6 +23,7 @@ import javax.annotation.concurrent.Immutable; import java.util.List; import java.util.Map; +import java.util.StringJoiner; /** * Represents an 'And' conditions condition operation. @@ -30,6 +31,7 @@ public class AndCondition implements Condition { private final List conditions; + private static final String OPERAND = "AND"; public AndCondition(@Nonnull List conditions) { this.conditions = conditions; @@ -67,6 +69,21 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return true; // otherwise, return true } + @Override + public String getOperandOrId() { + return OPERAND; + } + + @Override + public String toJson() { + StringJoiner s = new StringJoiner(", ", "[", "]"); + s.add("\"and\""); + for (int i = 0; i < conditions.size(); i++) { + s.add(conditions.get(i).toJson()); + } + return s.toString(); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java index c4f052ebb..e07757016 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java @@ -64,6 +64,11 @@ public String getAudienceId() { return audienceId; } + @Override + public String getOperandOrId() { + return audienceId; + } + @Nullable @Override public Boolean evaluate(ProjectConfig config, Map attributes) { @@ -101,4 +106,7 @@ public int hashCode() { public String toString() { return audienceId; } + + @Override + public String toJson() { return null; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java index 772d2b03e..11b7165b9 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java @@ -28,4 +28,8 @@ public interface Condition { @Nullable Boolean evaluate(ProjectConfig config, Map attributes); + + String toJson(); + + String getOperandOrId(); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java index 8f8aedeae..9bb355a13 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java @@ -27,4 +27,11 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return true; } + @Override + public String toJson() { return null; } + + @Override + public String getOperandOrId() { + return null; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java index b7f45f2ac..8c17b4ef2 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java @@ -23,6 +23,7 @@ import javax.annotation.Nonnull; import java.util.Map; +import java.util.StringJoiner; /** * Represents a 'Not' conditions condition operation. @@ -31,6 +32,7 @@ public class NotCondition implements Condition { private final Condition condition; + private static final String OPERAND = "NOT"; public NotCondition(@Nonnull Condition condition) { this.condition = condition; @@ -47,6 +49,19 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return (conditionEval == null ? null : !conditionEval); } + @Override + public String getOperandOrId() { + return OPERAND; + } + + @Override + public String toJson() { + StringJoiner s = new StringJoiner(", ","[","]"); + s.add("\"not\""); + s.add(condition.toJson()); + return s.toString(); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java index fcf5100db..10633aed9 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java @@ -26,4 +26,12 @@ public class NullCondition implements Condition { public Boolean evaluate(ProjectConfig config, Map attributes) { return null; } + + @Override + public String toJson() { return null; } + + @Override + public String getOperandOrId() { + return null; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java index 70572a9a9..71c8c9e76 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java @@ -23,6 +23,7 @@ import javax.annotation.concurrent.Immutable; import java.util.List; import java.util.Map; +import java.util.StringJoiner; /** * Represents an 'Or' conditions condition operation. @@ -30,6 +31,7 @@ @Immutable public class OrCondition implements Condition { private final List conditions; + private static final String OPERAND = "OR"; public OrCondition(@Nonnull List conditions) { this.conditions = conditions; @@ -65,6 +67,21 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return false; } + @Override + public String getOperandOrId() { + return OPERAND; + } + + @Override + public String toJson() { + StringJoiner s = new StringJoiner(", ", "[", "]"); + s.add("\"or\""); + for (int i = 0; i < conditions.size(); i++) { + s.add(conditions.get(i).toJson()); + } + return s.toString(); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index 277f2f184..ed029f89c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -119,19 +119,39 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { } @Override - public String toString() { + public String getOperandOrId() { + return null; + } + + public String getValueStr() { final String valueStr; if (value == null) { valueStr = "null"; } else if (value instanceof String) { - valueStr = String.format("'%s'", value); + valueStr = String.format("%s", value); } else { valueStr = value.toString(); } + return valueStr; + } + + @Override + public String toJson() { + StringBuilder attributes = new StringBuilder(); + if (name != null) attributes.append("{\"name\":\"" + name + "\""); + if (type != null) attributes.append(", \"type\":\"" + type + "\""); + if (match != null) attributes.append(", \"match\":\"" + match + "\""); + attributes.append(", \"value\":" + ((value instanceof String) ? ("\"" + getValueStr() + "\"") : getValueStr()) + "}"); + + return attributes.toString(); + } + + @Override + public String toString() { return "{name='" + name + "\'" + ", type='" + type + "\'" + ", match='" + match + "\'" + - ", value=" + valueStr + + ", value=" + ((value instanceof String) ? ("'" + getValueStr() + "'") : getValueStr()) + "}"; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ConditionJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ConditionJacksonDeserializer.java index ca57ac0af..f443d9d07 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ConditionJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ConditionJacksonDeserializer.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019, Optimizely and contributors + * Copyright 2018-2019, 2021, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,9 +121,6 @@ protected static Condition parseConditions(Class clazz, ObjectMapper obje case "and": condition = new AndCondition(conditions); break; - case "or": - condition = new OrCondition(conditions); - break; case "not": condition = new NotCondition(conditions.isEmpty() ? new NullCondition() : conditions.get(0)); break; diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ConditionUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ConditionUtils.java index d9af5b58b..32ab45cc4 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ConditionUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ConditionUtils.java @@ -1,6 +1,6 @@ /** * - * Copyright 2018-2019,2021, Optimizely and contributors + * Copyright 2018-2019, 2021, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,23 +147,7 @@ static public Condition parseConditions(Class clazz, List rawObje conditions.add(parseConditions(clazz, obj)); } - Condition condition; - switch (operand) { - case "and": - condition = new AndCondition(conditions); - break; - case "or": - condition = new OrCondition(conditions); - break; - case "not": - condition = new NotCondition(conditions.isEmpty() ? new NullCondition() : conditions.get(0)); - break; - default: - condition = new OrCondition(conditions); - break; - } - - return condition; + return buildCondition(operand, conditions); } static public String operand(Object object) { @@ -210,14 +194,15 @@ static public Condition parseConditions(Class clazz, org.json.JSONArray c conditions.add(parseConditions(clazz, obj)); } + return buildCondition(operand, conditions); + } + + private static Condition buildCondition(String operand, List conditions) { Condition condition; switch (operand) { case "and": condition = new AndCondition(conditions); break; - case "or": - condition = new OrCondition(conditions); - break; case "not": condition = new NotCondition(conditions.isEmpty() ? new NullCondition() : conditions.get(0)); break; diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttribute.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttribute.java new file mode 100644 index 000000000..2c142bc86 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttribute.java @@ -0,0 +1,53 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.ab.optimizelyconfig; + +import com.optimizely.ab.config.IdKeyMapped; + +/** + * Represents the Attribute's map {@link OptimizelyConfig} + */ +public class OptimizelyAttribute implements IdKeyMapped { + + private String id; + private String key; + + public OptimizelyAttribute(String id, + String key) { + this.id = id; + this.key = key; + } + + public String getId() { return id; } + + public String getKey() { return key; } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + OptimizelyAttribute optimizelyAttribute = (OptimizelyAttribute) obj; + return id.equals(optimizelyAttribute.getId()) && + key.equals(optimizelyAttribute.getKey()); + } + + @Override + public int hashCode() { + int hash = id.hashCode(); + hash = 31 * hash + key.hashCode(); + return hash; + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAudience.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAudience.java new file mode 100644 index 000000000..d874b900e --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyAudience.java @@ -0,0 +1,63 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.ab.optimizelyconfig; + +import com.optimizely.ab.config.IdKeyMapped; +import com.optimizely.ab.config.audience.Condition; + +import java.util.List; + +/** + * Represents the Audiences list {@link OptimizelyConfig} + */ +public class OptimizelyAudience{ + + private String id; + private String name; + private String conditions; + + public OptimizelyAudience(String id, + String name, + String conditions) { + this.id = id; + this.name = name; + this.conditions = conditions; + } + + public String getId() { return id; } + + public String getName() { return name; } + + public String getConditions() { return conditions; } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + OptimizelyAudience optimizelyAudience = (OptimizelyAudience) obj; + return id.equals(optimizelyAudience.getId()) && + name.equals(optimizelyAudience.getName()) && + conditions.equals(optimizelyAudience.getConditions()); + } + + @Override + public int hashCode() { + int hash = id.hashCode(); + hash = 31 * hash + conditions.hashCode(); + return hash; + } + +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfig.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfig.java index c1640ff44..7fa890b66 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfig.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import com.optimizely.ab.config.Attribute; +import com.optimizely.ab.config.EventType; import java.util.*; @@ -25,31 +27,40 @@ */ @JsonInclude(JsonInclude.Include.NON_NULL) public class OptimizelyConfig { - + private Map experimentsMap; private Map featuresMap; + private List attributes; + private List events; + private List audiences; private String revision; private String sdkKey; private String environmentKey; private String datafile; - public OptimizelyConfig(Map experimentsMap, - Map featuresMap, - String revision, String sdkKey, String environmentKey) { - this(experimentsMap, featuresMap, revision, sdkKey, environmentKey, null); - } - public OptimizelyConfig(Map experimentsMap, Map featuresMap, String revision, String sdkKey, String environmentKey, + List attributes, + List events, + List audiences, String datafile) { + + // This experimentsMap is for experiments of legacy projects only. + // For flag projects, experiment keys are not guaranteed to be unique + // across multiple flags, so this map may not include all experiments + // when keys conflict. this.experimentsMap = experimentsMap; + this.featuresMap = featuresMap; this.revision = revision; - this.sdkKey = sdkKey; - this.environmentKey = environmentKey; + this.sdkKey = sdkKey == null ? "" : sdkKey; + this.environmentKey = environmentKey == null ? "" : environmentKey; + this.attributes = attributes; + this.events = events; + this.audiences = audiences; this.datafile = datafile; } @@ -61,6 +72,12 @@ public Map getFeaturesMap() { return featuresMap; } + public List getAttributes() { return attributes; } + + public List getEvents() { return events; } + + public List getAudiences() { return audiences; } + public String getRevision() { return revision; } @@ -82,7 +99,10 @@ public boolean equals(Object obj) { OptimizelyConfig optimizelyConfig = (OptimizelyConfig) obj; return revision.equals(optimizelyConfig.getRevision()) && experimentsMap.equals(optimizelyConfig.getExperimentsMap()) && - featuresMap.equals(optimizelyConfig.getFeaturesMap()); + featuresMap.equals(optimizelyConfig.getFeaturesMap()) && + attributes.equals(optimizelyConfig.getAttributes()) && + events.equals(optimizelyConfig.getEvents()) && + audiences.equals(optimizelyConfig.getAudiences()); } @Override diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigService.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigService.java index af1965ce1..60aef9ce3 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigService.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigService.java @@ -17,23 +17,59 @@ import com.optimizely.ab.annotations.VisibleForTesting; import com.optimizely.ab.config.*; +import com.optimizely.ab.config.audience.Audience; + import java.util.*; public class OptimizelyConfigService { private ProjectConfig projectConfig; private OptimizelyConfig optimizelyConfig; + private List audiences; + private Map audiencesMap; + private Map> featureIdToVariablesMap = new HashMap<>(); + private Map experimentMapByExperimentId = new HashMap<>(); public OptimizelyConfigService(ProjectConfig projectConfig) { this.projectConfig = projectConfig; + this.audiences = getAudiencesList(projectConfig.getTypedAudiences(), projectConfig.getAudiences()); + this.audiencesMap = getAudiencesMap(this.audiences); + + List optimizelyAttributes = new ArrayList<>(); + List optimizelyEvents = new ArrayList<>(); Map experimentsMap = getExperimentsMap(); + + if (projectConfig.getAttributes() != null) { + for (Attribute attribute : projectConfig.getAttributes()) { + OptimizelyAttribute copyAttribute = new OptimizelyAttribute( + attribute.getId(), + attribute.getKey() + ); + optimizelyAttributes.add(copyAttribute); + } + } + + if (projectConfig.getEventTypes() != null) { + for (EventType event : projectConfig.getEventTypes()) { + OptimizelyEvent copyEvent = new OptimizelyEvent( + event.getId(), + event.getKey(), + event.getExperimentIds() + ); + optimizelyEvents.add(copyEvent); + } + } + optimizelyConfig = new OptimizelyConfig( experimentsMap, getFeaturesMap(experimentsMap), projectConfig.getRevision(), projectConfig.getSdkKey(), projectConfig.getEnvironmentKey(), + optimizelyAttributes, + optimizelyEvents, + this.audiences, projectConfig.toDatafile() ); } @@ -60,6 +96,7 @@ Map> generateFeatureKeyToVariablesMap() { Map> featureVariableIdMap = new HashMap<>(); for (FeatureFlag featureFlag : featureFlags) { featureVariableIdMap.put(featureFlag.getKey(), featureFlag.getVariables()); + featureIdToVariablesMap.put(featureFlag.getId(), featureFlag.getVariables()); } return featureVariableIdMap; } @@ -73,33 +110,39 @@ String getExperimentFeatureKey(String experimentId) { @VisibleForTesting Map getExperimentsMap() { List experiments = projectConfig.getExperiments(); + if (experiments == null) { return Collections.emptyMap(); } Map featureExperimentMap = new HashMap<>(); + for (Experiment experiment : experiments) { - featureExperimentMap.put(experiment.getKey(), new OptimizelyExperiment( + OptimizelyExperiment optimizelyExperiment = new OptimizelyExperiment( experiment.getId(), experiment.getKey(), - getVariationsMap(experiment.getVariations(), experiment.getId()) - )); + getVariationsMap(experiment.getVariations(), experiment.getId(), null), + experiment.serializeConditions(this.audiencesMap) + ); + + featureExperimentMap.put(experiment.getKey(), optimizelyExperiment); + experimentMapByExperimentId.put(experiment.getId(), optimizelyExperiment); } return featureExperimentMap; } @VisibleForTesting - Map getVariationsMap(List variations, String experimentId) { + Map getVariationsMap(List variations, String experimentId, String featureId) { if (variations == null) { return Collections.emptyMap(); } - Boolean isFeatureExperiment = this.getExperimentFeatureKey(experimentId) != null; + Map variationKeyMap = new HashMap<>(); for (Variation variation : variations) { variationKeyMap.put(variation.getKey(), new OptimizelyVariation( variation.getId(), variation.getKey(), - isFeatureExperiment ? variation.getFeatureEnabled() : null, - getMergedVariablesMap(variation, experimentId) + variation.getFeatureEnabled(), + getMergedVariablesMap(variation, experimentId, featureId) )); } return variationKeyMap; @@ -112,36 +155,41 @@ Map getVariationsMap(List variations, St * 3. If Variation does not contain a variable, then all `id`, `key`, `type` and defaultValue as `value` is used from feature varaible and added to variation. */ @VisibleForTesting - Map getMergedVariablesMap(Variation variation, String experimentId) { + Map getMergedVariablesMap(Variation variation, String experimentId, String featureId) { String featureKey = this.getExperimentFeatureKey(experimentId); - if (featureKey != null) { - // Map containing variables list for every feature key used for merging variation and feature variables. - Map> featureKeyToVariablesMap = generateFeatureKeyToVariablesMap(); - - // Generate temp map of all the available variable values from variation. - Map tempVariableIdMap = getFeatureVariableUsageInstanceMap(variation.getFeatureVariableUsageInstances()); - - // Iterate over all the variables available in associated feature. - // Use value from variation variable if variable is available in variation and feature is enabled, otherwise use defaultValue from feature variable. - List featureVariables = featureKeyToVariablesMap.get(featureKey); - if (featureVariables == null) { - return Collections.emptyMap(); - } + Map> featureKeyToVariablesMap = generateFeatureKeyToVariablesMap(); + if (featureKey == null && featureId == null) { + return Collections.emptyMap(); + } - Map featureVariableKeyMap = new HashMap<>(); - for (FeatureVariable featureVariable : featureVariables) { - featureVariableKeyMap.put(featureVariable.getKey(), new OptimizelyVariable( - featureVariable.getId(), - featureVariable.getKey(), - featureVariable.getType(), - variation.getFeatureEnabled() && tempVariableIdMap.get(featureVariable.getId()) != null - ? tempVariableIdMap.get(featureVariable.getId()).getValue() - : featureVariable.getDefaultValue() - )); - } - return featureVariableKeyMap; + // Generate temp map of all the available variable values from variation. + Map tempVariableIdMap = getFeatureVariableUsageInstanceMap(variation.getFeatureVariableUsageInstances()); + + // Iterate over all the variables available in associated feature. + // Use value from variation variable if variable is available in variation and feature is enabled, otherwise use defaultValue from feature variable. + List featureVariables; + + if (featureId != null) { + featureVariables = featureIdToVariablesMap.get(featureId); + } else { + featureVariables = featureKeyToVariablesMap.get(featureKey); } - return Collections.emptyMap(); + if (featureVariables == null) { + return Collections.emptyMap(); + } + + Map featureVariableKeyMap = new HashMap<>(); + for (FeatureVariable featureVariable : featureVariables) { + featureVariableKeyMap.put(featureVariable.getKey(), new OptimizelyVariable( + featureVariable.getId(), + featureVariable.getKey(), + featureVariable.getType(), + variation.getFeatureEnabled() && tempVariableIdMap.get(featureVariable.getId()) != null + ? tempVariableIdMap.get(featureVariable.getId()).getValue() + : featureVariable.getDefaultValue() + )); + } + return featureVariableKeyMap; } @VisibleForTesting @@ -172,16 +220,52 @@ Map getFeaturesMap(Map Map optimizelyFeatureKeyMap = new HashMap<>(); for (FeatureFlag featureFlag : featureFlags) { - optimizelyFeatureKeyMap.put(featureFlag.getKey(), new OptimizelyFeature( + Map experimentsMapForFeature = + getExperimentsMapForFeature(featureFlag.getExperimentIds(), allExperimentsMap); + + List experimentRules = + new ArrayList(experimentsMapForFeature.values()); + List deliveryRules = + this.getDeliveryRules(featureFlag.getRolloutId(), featureFlag.getId()); + + OptimizelyFeature optimizelyFeature = new OptimizelyFeature( featureFlag.getId(), featureFlag.getKey(), - getExperimentsMapForFeature(featureFlag.getExperimentIds(), allExperimentsMap), - getFeatureVariablesMap(featureFlag.getVariables()) - )); + experimentsMapForFeature, + getFeatureVariablesMap(featureFlag.getVariables()), + experimentRules, + deliveryRules + ); + + optimizelyFeatureKeyMap.put(featureFlag.getKey(), optimizelyFeature); } return optimizelyFeatureKeyMap; } + List getDeliveryRules(String rolloutId, String featureId) { + + List deliveryRules = new ArrayList(); + + Rollout rollout = projectConfig.getRolloutIdMapping().get(rolloutId); + + if (rollout != null) { + List rolloutExperiments = rollout.getExperiments(); + for (Experiment experiment: rolloutExperiments) { + OptimizelyExperiment optimizelyExperiment = new OptimizelyExperiment( + experiment.getId(), + experiment.getKey(), + this.getVariationsMap(experiment.getVariations(), experiment.getId(), featureId), + experiment.serializeConditions(this.audiencesMap) + ); + + deliveryRules.add(optimizelyExperiment); + } + return deliveryRules; + } + + return Collections.emptyList(); + } + @VisibleForTesting Map getExperimentsMapForFeature(List experimentIds, Map allExperimentsMap) { if (experimentIds == null) { @@ -190,8 +274,8 @@ Map getExperimentsMapForFeature(List exper Map optimizelyExperimentKeyMap = new HashMap<>(); for (String experimentId : experimentIds) { - String experimentKey = projectConfig.getExperimentIdMapping().get(experimentId).getKey(); - optimizelyExperimentKeyMap.put(experimentKey, allExperimentsMap.get(experimentKey)); + Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); + optimizelyExperimentKeyMap.put(experiment.getKey(), experimentMapByExperimentId.get(experiment.getId())); } return optimizelyExperimentKeyMap; @@ -215,4 +299,60 @@ Map getFeatureVariablesMap(List fea return featureVariableKeyMap; } + + @VisibleForTesting + List getAudiencesList(List typedAudiences, List audiences) { + /* + * This method merges typedAudiences with audiences from the Project + * config. Precedence is given to typedAudiences over audiences. + * + * Returns: + * A new list with the merged audiences as OptimizelyAudience objects. + * */ + List audiencesList = new ArrayList<>(); + Map idLookupMap = new HashMap<>(); + if (typedAudiences != null) { + for (Audience audience : typedAudiences) { + OptimizelyAudience optimizelyAudience = new OptimizelyAudience( + audience.getId(), + audience.getName(), + audience.getConditions().toJson() + ); + audiencesList.add(optimizelyAudience); + idLookupMap.put(audience.getId(), audience.getId()); + } + } + + if (audiences != null) { + for (Audience audience : audiences) { + if (!idLookupMap.containsKey(audience.getId()) && !audience.getId().equals("$opt_dummy_audience")) { + OptimizelyAudience optimizelyAudience = new OptimizelyAudience( + audience.getId(), + audience.getName(), + audience.getConditions().toJson() + ); + audiencesList.add(optimizelyAudience); + } + } + } + + return audiencesList; + } + + @VisibleForTesting + Map getAudiencesMap(List optimizelyAudiences) { + Map audiencesMap = new HashMap<>(); + + // Build audienceMap as [id:name] + if (optimizelyAudiences != null) { + for (OptimizelyAudience audience : optimizelyAudiences) { + audiencesMap.put( + audience.getId(), + audience.getName() + ); + } + } + + return audiencesMap; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyEvent.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyEvent.java new file mode 100644 index 000000000..9edda8700 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyEvent.java @@ -0,0 +1,61 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.ab.optimizelyconfig; + +import com.optimizely.ab.config.IdKeyMapped; + +import java.util.List; + +/** + * Represents the Events's map {@link OptimizelyConfig} + */ +public class OptimizelyEvent implements IdKeyMapped { + + private String id; + private String key; + private List experimentIds; + + public OptimizelyEvent(String id, + String key, + List experimentIds) { + this.id = id; + this.key = key; + this.experimentIds = experimentIds; + } + + public String getId() { return id; } + + public String getKey() { return key; } + + public List getExperimentIds() { return experimentIds; } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + OptimizelyEvent optimizelyEvent = (OptimizelyEvent) obj; + return id.equals(optimizelyEvent.getId()) && + key.equals(optimizelyEvent.getKey()) && + experimentIds.equals(optimizelyEvent.getExperimentIds()); + } + + @Override + public int hashCode() { + int hash = id.hashCode(); + hash = 31 * hash + experimentIds.hashCode(); + return hash; + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperiment.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperiment.java index fd66e9aab..0f5b9e193 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperiment.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperiment.java @@ -26,12 +26,14 @@ public class OptimizelyExperiment implements IdKeyMapped { private String id; private String key; + private String audiences = ""; private Map variationsMap; - public OptimizelyExperiment(String id, String key, Map variationsMap) { + public OptimizelyExperiment(String id, String key, Map variationsMap, String audiences) { this.id = id; this.key = key; this.variationsMap = variationsMap; + this.audiences = audiences; } public String getId() { @@ -42,6 +44,8 @@ public String getKey() { return key; } + public String getAudiences() { return audiences; } + public Map getVariationsMap() { return variationsMap; } @@ -53,7 +57,8 @@ public boolean equals(Object obj) { OptimizelyExperiment optimizelyExperiment = (OptimizelyExperiment) obj; return id.equals(optimizelyExperiment.getId()) && key.equals(optimizelyExperiment.getKey()) && - variationsMap.equals(optimizelyExperiment.getVariationsMap()); + variationsMap.equals(optimizelyExperiment.getVariationsMap()) && + audiences.equals(optimizelyExperiment.getAudiences()); } @Override diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeature.java b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeature.java index 954f7b14e..1f8359e62 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeature.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeature.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2020, Optimizely, Inc. and contributors * + * Copyright 2020-2021, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -17,6 +17,8 @@ import com.optimizely.ab.config.IdKeyMapped; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -27,17 +29,28 @@ public class OptimizelyFeature implements IdKeyMapped { private String id; private String key; + private List deliveryRules; + private List experimentRules; + + /** + * @deprecated use {@link #experimentRules} and {@link #deliveryRules} instead + */ + @Deprecated private Map experimentsMap; private Map variablesMap; public OptimizelyFeature(String id, - String key, - Map experimentsMap, - Map variablesMap) { + String key, + Map experimentsMap, + Map variablesMap, + List experimentRules, + List deliveryRules) { this.id = id; this.key = key; this.experimentsMap = experimentsMap; this.variablesMap = variablesMap; + this.experimentRules = experimentRules; + this.deliveryRules = deliveryRules; } public String getId() { @@ -56,6 +69,10 @@ public Map getVariablesMap() { return variablesMap; } + public List getExperimentRules() { return experimentRules; } + + public List getDeliveryRules() { return deliveryRules; } + @Override public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; @@ -64,13 +81,19 @@ public boolean equals(Object obj) { return id.equals(optimizelyFeature.getId()) && key.equals(optimizelyFeature.getKey()) && experimentsMap.equals(optimizelyFeature.getExperimentsMap()) && - variablesMap.equals(optimizelyFeature.getVariablesMap()); + variablesMap.equals(optimizelyFeature.getVariablesMap()) && + experimentRules.equals(optimizelyFeature.getExperimentRules()) && + deliveryRules.equals(optimizelyFeature.getDeliveryRules()); } @Override public int hashCode() { int result = id.hashCode(); - result = 31 * result + experimentsMap.hashCode() + variablesMap.hashCode(); + result = 31 * result + + experimentsMap.hashCode() + + variablesMap.hashCode() + + experimentRules.hashCode() + + deliveryRules.hashCode(); return result; } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/ExperimentTest.java b/core-api/src/test/java/com/optimizely/ab/config/ExperimentTest.java new file mode 100644 index 000000000..334e76067 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/ExperimentTest.java @@ -0,0 +1,205 @@ +/** + * + * Copyright 2021, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config; + +import com.optimizely.ab.config.audience.*; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.*; + +public class ExperimentTest { + + @Test + public void testStringifyConditionScenarios() { + List audienceConditionsScenarios = getAudienceConditionsList(); + Map expectedScenarioStringsMap = getExpectedScenariosMap(); + Map audiencesMap = new HashMap<>(); + audiencesMap.put("1", "us"); + audiencesMap.put("2", "female"); + audiencesMap.put("3", "adult"); + audiencesMap.put("11", "fr"); + audiencesMap.put("12", "male"); + audiencesMap.put("13", "kid"); + + if (expectedScenarioStringsMap.size() == audienceConditionsScenarios.size()) { + for (int i = 0; i < audienceConditionsScenarios.size() - 1; i++) { + Experiment experiment = makeMockExperimentWithStatus(Experiment.ExperimentStatus.RUNNING, + audienceConditionsScenarios.get(i)); + String audiences = experiment.serializeConditions(audiencesMap); + assertEquals(expectedScenarioStringsMap.get(i+1), audiences); + } + } + + } + + public Map getExpectedScenariosMap() { + Map expectedScenarioStringsMap = new HashMap<>(); + expectedScenarioStringsMap.put(1, ""); + expectedScenarioStringsMap.put(2, "\"us\" OR \"female\""); + expectedScenarioStringsMap.put(3, "\"us\" AND \"female\" AND \"adult\""); + expectedScenarioStringsMap.put(4, "NOT \"us\""); + expectedScenarioStringsMap.put(5, "\"us\""); + expectedScenarioStringsMap.put(6, "\"us\""); + expectedScenarioStringsMap.put(7, "\"us\""); + expectedScenarioStringsMap.put(8, "\"us\" OR \"female\""); + expectedScenarioStringsMap.put(9, "(\"us\" OR \"female\") AND \"adult\""); + expectedScenarioStringsMap.put(10, "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))"); + expectedScenarioStringsMap.put(11, "NOT (\"us\" AND \"female\")"); + expectedScenarioStringsMap.put(12, "\"us\" OR \"100000\""); + expectedScenarioStringsMap.put(13, ""); + + return expectedScenarioStringsMap; + } + + public List getAudienceConditionsList() { + AudienceIdCondition one = new AudienceIdCondition("1"); + AudienceIdCondition two = new AudienceIdCondition("2"); + AudienceIdCondition three = new AudienceIdCondition("3"); + AudienceIdCondition eleven = new AudienceIdCondition("11"); + AudienceIdCondition twelve = new AudienceIdCondition("12"); + AudienceIdCondition thirteen = new AudienceIdCondition("13"); + + // Scenario 1 - [] + EmptyCondition scenario1 = new EmptyCondition(); + + // Scenario 2 - ["or", "1", "2"] + List scenario2List = new ArrayList<>(); + scenario2List.add(one); + scenario2List.add(two); + OrCondition scenario2 = new OrCondition(scenario2List); + + // Scenario 3 - ["and", "1", "2", "3"] + List scenario3List = new ArrayList<>(); + scenario3List.add(one); + scenario3List.add(two); + scenario3List.add(three); + AndCondition scenario3 = new AndCondition(scenario3List); + + // Scenario 4 - ["not", "1"] + NotCondition scenario4 = new NotCondition(one); + + // Scenario 5 - ["or", "1"] + List scenario5List = new ArrayList<>(); + scenario5List.add(one); + OrCondition scenario5 = new OrCondition(scenario5List); + + // Scenario 6 - ["and", "1"] + List scenario6List = new ArrayList<>(); + scenario6List.add(one); + AndCondition scenario6 = new AndCondition(scenario6List); + + // Scenario 7 - ["1"] + AudienceIdCondition scenario7 = one; + + // Scenario 8 - ["1", "2"] + // Defaults to Or in Datafile Parsing resulting in an OrCondition + // Same as Scenario 2 + + OrCondition scenario8 = scenario2; + + // Scenario 9 - ["and", ["or", "1", "2"], "3"] + List Scenario9List = new ArrayList<>(); + Scenario9List.add(scenario2); + Scenario9List.add(three); + AndCondition scenario9 = new AndCondition(Scenario9List); + + // Scenario 10 - ["and", ["or", "1", ["and", "2", "3"]], ["and", "11, ["or", "12", "13"]]] + List scenario10List = new ArrayList<>(); + + List or1213List = new ArrayList<>(); + or1213List.add(twelve); + or1213List.add(thirteen); + OrCondition or1213 = new OrCondition(or1213List); + + List and11Or1213List = new ArrayList<>(); + and11Or1213List.add(eleven); + and11Or1213List.add(or1213); + AndCondition and11Or1213 = new AndCondition(and11Or1213List); + + List and23List = new ArrayList<>(); + and23List.add(two); + and23List.add(three); + AndCondition and23 = new AndCondition(and23List); + + List or1And23List = new ArrayList<>(); + or1And23List.add(one); + or1And23List.add(and23); + OrCondition or1And23 = new OrCondition(or1And23List); + + scenario10List.add(or1And23); + scenario10List.add(and11Or1213); + AndCondition scenario10 = new AndCondition(scenario10List); + + // Scenario 11 - ["not", ["and", "1", "2"]] + List and12List = new ArrayList<>(); + and12List.add(one); + and12List.add(two); + AndCondition and12 = new AndCondition(and12List); + + NotCondition scenario11 = new NotCondition(and12); + + // Scenario 12 - ["or", "1", "100000"] + List scenario12List = new ArrayList<>(); + scenario12List.add(one); + AudienceIdCondition unknownAudience = new AudienceIdCondition("100000"); + scenario12List.add(unknownAudience); + + OrCondition scenario12 = new OrCondition(scenario12List); + + // Scenario 13 - ["and", ["and", invalidAudienceIdCondition]] which becomes + // the scenario of ["and", "and"] and results in empty string. + AudienceIdCondition invalidAudience = new AudienceIdCondition("5"); + List invalidIdList = new ArrayList<>(); + invalidIdList.add(invalidAudience); + AndCondition andCondition = new AndCondition(invalidIdList); + List andInvalidAudienceId = new ArrayList<>(); + andInvalidAudienceId.add(andCondition); + AndCondition scenario13 = new AndCondition(andInvalidAudienceId); + + + List conditionTestScenarios = new ArrayList<>(); + conditionTestScenarios.add(scenario1); + conditionTestScenarios.add(scenario2); + conditionTestScenarios.add(scenario3); + conditionTestScenarios.add(scenario4); + conditionTestScenarios.add(scenario5); + conditionTestScenarios.add(scenario6); + conditionTestScenarios.add(scenario7); + conditionTestScenarios.add(scenario8); + conditionTestScenarios.add(scenario9); + conditionTestScenarios.add(scenario10); + conditionTestScenarios.add(scenario11); + conditionTestScenarios.add(scenario12); + conditionTestScenarios.add(scenario13); + + return conditionTestScenarios; + } + + private Experiment makeMockExperimentWithStatus(Experiment.ExperimentStatus status, Condition audienceConditions) { + return new Experiment("12345", + "mockExperimentKey", + status.toString(), + "layerId", + Collections.emptyList(), + audienceConditions, + Collections.emptyList(), + Collections.emptyMap(), + Collections.emptyList() + ); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index 0a6e41ddc..80d4ef9d9 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -56,6 +56,17 @@ public void initialize() { testTypedUserAttributes.put("null_val", null); } + /** + * Verify that UserAttribute.toJson returns a json represented string of conditions. + */ + @Test + public void userAttributeConditionsToJson() throws Exception { + UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "true", "safari"); + String expectedConditionJsonString = "{\"name\":\"browser_type\", \"type\":\"custom_attribute\", \"match\":\"true\", \"value\":\"safari\"}"; + assertEquals(testInstance.toJson(), expectedConditionJsonString); + } + + /** * Verify that UserAttribute.evaluate returns true on exact-matching visitor attribute data. */ diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttributeTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttributeTest.java new file mode 100644 index 000000000..904d5e2d7 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyAttributeTest.java @@ -0,0 +1,39 @@ +/**************************************************************************** + * Copyright 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.ab.optimizelyconfig; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class OptimizelyAttributeTest { + + @Test + public void testOptimizelyAttribute() { + OptimizelyAttribute optimizelyAttribute1 = new OptimizelyAttribute( + "5", + "test_attribute" + ); + OptimizelyAttribute optimizelyAttribute2 = new OptimizelyAttribute( + "5", + "test_attribute" + ); + assertEquals("5", optimizelyAttribute1.getId()); + assertEquals("test_attribute", optimizelyAttribute1.getKey()); + assertEquals(optimizelyAttribute1, optimizelyAttribute2); + assertEquals(optimizelyAttribute1.hashCode(), optimizelyAttribute2.hashCode()); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java index 52bc06181..e52436b33 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java @@ -124,7 +124,7 @@ public void testGetFeatureVariableUsageInstanceMap() { @Test public void testGetVariationsMap() { Map optimizelyVariationMap = - optimizelyConfigService.getVariationsMap(projectConfig.getExperiments().get(1).getVariations(), "3262035800"); + optimizelyConfigService.getVariationsMap(projectConfig.getExperiments().get(1).getVariations(), "3262035800", null); assertEquals(expectedConfig.getExperimentsMap().get("multivariate_experiment").getVariationsMap().size(), optimizelyVariationMap.size()); assertEquals(expectedConfig.getExperimentsMap().get("multivariate_experiment").getVariationsMap(), optimizelyVariationMap); } @@ -149,13 +149,30 @@ public void testGenerateFeatureKeyToVariablesMap() { @Test public void testGetMergedVariablesMap() { Variation variation = projectConfig.getExperiments().get(1).getVariations().get(1); - Map optimizelyVariableMap = optimizelyConfigService.getMergedVariablesMap(variation, "3262035800"); + Map optimizelyVariableMap = optimizelyConfigService.getMergedVariablesMap(variation, "3262035800", null); Map expectedOptimizelyVariableMap = expectedConfig.getExperimentsMap().get("multivariate_experiment").getVariationsMap().get("Feorge").getVariablesMap(); assertEquals(expectedOptimizelyVariableMap.size(), optimizelyVariableMap.size()); assertEquals(expectedOptimizelyVariableMap, optimizelyVariableMap); } + @Test + public void testGetAudiencesMap() { + Map actualAudiencesMap = optimizelyConfigService.getAudiencesMap( + asList( + new OptimizelyAudience( + "123456", + "test_audience_1", + "[\"and\", [\"or\", \"1\", \"2\"], \"3\"]" + ) + ) + ); + + Map expectedAudiencesMap = optimizelyConfigService.getAudiencesMap(expectedConfig.getAudiences()); + + assertEquals(expectedAudiencesMap, actualAudiencesMap); + } + private ProjectConfig generateOptimizelyConfig() { return new DatafileProjectConfig( "2360254204", @@ -385,7 +402,8 @@ OptimizelyConfig getExpectedConfig() { }} ) ); - }} + }}, + "" ) ); optimizelyExperimentMap.put( @@ -412,7 +430,8 @@ OptimizelyConfig getExpectedConfig() { Collections.emptyMap() ) ); - }} + }}, + "" ) ); @@ -485,7 +504,8 @@ OptimizelyConfig getExpectedConfig() { }} ) ); - }} + }}, + "" ) ); }}, @@ -508,7 +528,73 @@ OptimizelyConfig getExpectedConfig() { "arry" ) ); - }} + }}, + asList( + new OptimizelyExperiment( + "3262035800", + "multivariate_experiment", + new HashMap() {{ + put( + "Feorge", + new OptimizelyVariation( + "3631049532", + "Feorge", + true, + new HashMap() {{ + put( + "first_letter", + new OptimizelyVariable( + "675244127", + "first_letter", + "string", + "F" + ) + ); + put( + "rest_of_name", + new OptimizelyVariable( + "4052219963", + "rest_of_name", + "string", + "eorge" + ) + ); + }} + ) + ); + put( + "Fred", + new OptimizelyVariation( + "1880281238", + "Fred", + true, + new HashMap() {{ + put( + "first_letter", + new OptimizelyVariable( + "675244127", + "first_letter", + "string", + "F" + ) + ); + put( + "rest_of_name", + new OptimizelyVariable( + "4052219963", + "rest_of_name", + "string", + "red" + ) + ); + }} + ) + ); + }}, + "" + ) + ), + Collections.emptyList() ) ); optimizelyFeatureMap.put( @@ -517,7 +603,9 @@ OptimizelyConfig getExpectedConfig() { "4195505407", "boolean_feature", Collections.emptyMap(), - Collections.emptyMap() + Collections.emptyMap(), + Collections.emptyList(), + Collections.emptyList() ) ); @@ -526,7 +614,37 @@ OptimizelyConfig getExpectedConfig() { optimizelyFeatureMap, "1480511547", "ValidProjectConfigV4", - "production" + "production", + asList( + new OptimizelyAttribute( + "553339214", + "house" + ), + new OptimizelyAttribute( + "58339410", + "nationality" + ) + ), + asList( + new OptimizelyEvent( + "3785620495", + "basic_event", + asList("1323241596", "2738374745", "3042640549", "3262035800", "3072915611") + ), + new OptimizelyEvent( + "3195631717", + "event_with_paused_experiment", + asList("2667098701") + ) + ), + asList( + new OptimizelyAudience( + "123456", + "test_audience_1", + "[\"and\", [\"or\", \"1\", \"2\"], \"3\"]" + ) + ), + null ); } } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigTest.java index 13b703799..58acadd3f 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigTest.java @@ -17,6 +17,7 @@ import org.junit.Test; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import static com.optimizely.ab.optimizelyconfig.OptimizelyExperimentTest.generateVariationMap; @@ -32,7 +33,11 @@ public void testOptimizelyConfig() { generateFeatureMap(), "101", "testingSdkKey", - "development" + "development", + null, + null, + null, + null ); assertEquals("101", optimizelyConfig.getRevision()); assertEquals("testingSdkKey", optimizelyConfig.getSdkKey()); @@ -53,12 +58,14 @@ private Map generateExperimentMap() { optimizelyExperimentMap.put("test_exp_1", new OptimizelyExperiment( "33", "test_exp_1", - generateVariationMap() + generateVariationMap(), + "" )); optimizelyExperimentMap.put("test_exp_2", new OptimizelyExperiment( "34", "test_exp_2", - generateVariationMap() + generateVariationMap(), + "" )); return optimizelyExperimentMap; } @@ -69,7 +76,9 @@ private Map generateFeatureMap() { "42", "test_feature_1", generateExperimentMap(), - generateVariablesMap() + generateVariablesMap(), + Collections.emptyList(), + Collections.emptyList() )); return optimizelyFeatureMap; } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyEventTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyEventTest.java new file mode 100644 index 000000000..5bd5d9a4c --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyEventTest.java @@ -0,0 +1,40 @@ +/**************************************************************************** + * Copyright 2020-2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +package com.optimizely.ab.optimizelyconfig; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static java.util.Arrays.asList; + +public class OptimizelyEventTest { + @Test + public void testOptimizelyEvent() { + OptimizelyEvent optimizelyEvent1 = new OptimizelyEvent( + "5", + "test_event", + asList("123","234","345") + ); + OptimizelyEvent optimizelyEvent2 = new OptimizelyEvent( + "5", + "test_event", + asList("123","234","345") + ); + assertEquals("5", optimizelyEvent1.getId()); + assertEquals("test_event", optimizelyEvent1.getKey()); + assertEquals(optimizelyEvent1, optimizelyEvent2); + assertEquals(optimizelyEvent1.hashCode(), optimizelyEvent2.hashCode()); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperimentTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperimentTest.java index ec7cebb79..954a90f29 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperimentTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyExperimentTest.java @@ -29,7 +29,8 @@ public void testOptimizelyExperiment() { OptimizelyExperiment optimizelyExperiment = new OptimizelyExperiment( "31", "test_exp", - generateVariationMap() + generateVariationMap(), + "" ); assertEquals("31", optimizelyExperiment.getId()); assertEquals("test_exp", optimizelyExperiment.getKey()); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeatureTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeatureTest.java index 732266a98..a6789311b 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeatureTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyFeatureTest.java @@ -17,6 +17,7 @@ import org.junit.Test; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import static com.optimizely.ab.optimizelyconfig.OptimizelyVariationTest.generateVariablesMap; @@ -31,7 +32,9 @@ public void testOptimizelyFeature() { "41", "test_feature", generateExperimentMap(), - generateVariablesMap() + generateVariablesMap(), + Collections.emptyList(), + Collections.emptyList() ); assertEquals("41", optimizelyFeature.getId()); assertEquals("test_feature", optimizelyFeature.getKey()); @@ -50,12 +53,14 @@ static Map generateExperimentMap() { optimizelyExperimentMap.put("test_exp_1", new OptimizelyExperiment ( "32", "test_exp_1", - generateVariationMap() + generateVariationMap(), + "" )); optimizelyExperimentMap.put("test_exp_2", new OptimizelyExperiment ( "33", "test_exp_2", - generateVariationMap() + generateVariationMap(), + "" )); return optimizelyExperimentMap; }