diff --git a/core-api/src/main/java/com/optimizely/ab/config/Layer.java b/core-api/src/main/java/com/optimizely/ab/config/Layer.java deleted file mode 100644 index dc4c476b4..000000000 --- a/core-api/src/main/java/com/optimizely/ab/config/Layer.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * - * Copyright 2017, 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.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -import javax.annotation.concurrent.Immutable; - -/** - * Represents a Optimizely Layer configuration - * - * @see Project JSON - */ -@Immutable -@JsonIgnoreProperties(ignoreUnknown = true) -public class Layer implements IdMapped { - - protected final String id; - protected final String policy; - protected final List experiments; - - public static final String SINGLE_EXPERIMENT_POLICY = "single_experiment"; - - @JsonCreator - public Layer(@JsonProperty("id") String id, - @JsonProperty("policy") String policy, - @JsonProperty("experiments") List experiments) { - this.id = id; - this.policy = policy; - this.experiments = experiments; - } - - public String getId() { - return id; - } - - public String getPolicy() { - return policy; - } - - public List getExperiments() { - return experiments; - } - - @Override - public String toString() { - return "Layer{" + - "id='" + id + '\'' + - ", policy='" + policy + '\'' + - ", experiments=" + experiments + - '}'; - } -} diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index ffb891e29..77e69ad2e 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -66,6 +66,7 @@ public String toString() { private final List featureFlags; private final List groups; private final List liveVariables; + private final List rollouts; // key to entity mappings private final Map attributeKeyMapping; @@ -108,7 +109,8 @@ public ProjectConfig(String accountId, String projectId, String version, String experiments, null, groups, - liveVariables + liveVariables, + null ); } @@ -124,7 +126,8 @@ public ProjectConfig(String accountId, List experiments, List featureFlags, List groups, - List liveVariables) { + List liveVariables, + List rollouts) { this.accountId = accountId; this.projectId = projectId; @@ -141,6 +144,12 @@ public ProjectConfig(String accountId, else { this.featureFlags = Collections.unmodifiableList(featureFlags); } + if (rollouts == null) { + this.rollouts = Collections.emptyList(); + } + else { + this.rollouts = Collections.unmodifiableList(rollouts); + } this.groups = Collections.unmodifiableList(groups); @@ -243,6 +252,10 @@ public List getFeatureFlags() { return featureFlags; } + public List getRollouts() { + return rollouts; + } + public List getAttributes() { return attributes; } @@ -312,22 +325,26 @@ public String toString() { ", projectId='" + projectId + '\'' + ", revision='" + revision + '\'' + ", version='" + version + '\'' + - ", anonymizeIP='" + anonymizeIP + '\'' + - ", groups=" + groups + - ", experiments=" + experiments + + ", anonymizeIP=" + anonymizeIP + ", attributes=" + attributes + - ", events=" + events + ", audiences=" + audiences + + ", events=" + events + + ", experiments=" + experiments + + ", featureFlags=" + featureFlags + + ", groups=" + groups + ", liveVariables=" + liveVariables + - ", experimentKeyMapping=" + experimentKeyMapping + + ", rollouts=" + rollouts + ", attributeKeyMapping=" + attributeKeyMapping + - ", liveVariableKeyMapping=" + liveVariableKeyMapping + ", eventNameMapping=" + eventNameMapping + + ", experimentKeyMapping=" + experimentKeyMapping + + ", featureKeyMapping=" + featureKeyMapping + + ", liveVariableKeyMapping=" + liveVariableKeyMapping + ", audienceIdMapping=" + audienceIdMapping + ", experimentIdMapping=" + experimentIdMapping + ", groupIdMapping=" + groupIdMapping + ", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping + ", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping + + ", variationIdToExperimentMapping=" + variationIdToExperimentMapping + '}'; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/Rollout.java b/core-api/src/main/java/com/optimizely/ab/config/Rollout.java index 06b8af1b3..b36f33838 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Rollout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Rollout.java @@ -16,6 +16,10 @@ */ package com.optimizely.ab.config; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + import javax.annotation.concurrent.Immutable; import java.util.List; @@ -25,19 +29,32 @@ * @see Project JSON */ @Immutable -public class Rollout extends Layer implements IdMapped { +@JsonIgnoreProperties(ignoreUnknown = true) +public class Rollout implements IdMapped { + + private final String id; + private final List experiments; + + @JsonCreator + public Rollout(@JsonProperty("id") String id, + @JsonProperty("experiments") List experiments) { + this.id = id; + this.experiments = experiments; + } + + @Override + public String getId() { + return id; + } - public Rollout(String id, - String policy, - List experiments) { - super(id, policy, experiments); + public List getExperiments() { + return experiments; } @Override public String toString() { return "Rollout{" + "id='" + id + '\'' + - ", policy='" + policy + '\'' + ", experiments=" + experiments + '}'; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index 79d486f09..1b2af1079 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -27,6 +27,7 @@ import com.optimizely.ab.config.LiveVariable.VariableType; import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; import com.optimizely.ab.config.audience.AndCondition; @@ -79,8 +80,10 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse } List featureFlags = null; + List rollouts = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = parseFeatureFlags(rootObject.getJSONArray("featureFlags")); + rollouts = parseRollouts(rootObject.getJSONArray("rollouts")); } return new ProjectConfig( @@ -95,7 +98,8 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse experiments, featureFlags, groups, - liveVariables + liveVariables, + rollouts ); } catch (Exception e) { throw new ConfigParseException("Unable to parse datafile: " + json, e); @@ -344,4 +348,18 @@ private List parseLiveVariableInstances(JSONArray liv return liveVariableUsageInstances; } + + private List parseRollouts(JSONArray rolloutsJson) { + List rollouts = new ArrayList(rolloutsJson.length()); + + for (Object obj : rolloutsJson) { + JSONObject rolloutObject = (JSONObject) obj; + String id = rolloutObject.getString("id"); + List experiments = parseExperiments(rolloutObject.getJSONArray("experiments")); + + rollouts.add(new Rollout(id, experiments)); + } + + return rollouts; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index 736ab80ad..be106665d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -27,6 +27,7 @@ import com.optimizely.ab.config.LiveVariable.VariableType; import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; import com.optimizely.ab.config.audience.AndCondition; @@ -81,8 +82,10 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse } List featureFlags = null; + List rollouts = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = parseFeatureFlags((JSONArray) rootObject.get("featureFlags")); + rollouts = parseRollouts((JSONArray) rootObject.get("rollouts")); } return new ProjectConfig( @@ -97,7 +100,8 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse experiments, featureFlags, groups, - liveVariables + liveVariables, + rollouts ); } catch (Exception e) { throw new ConfigParseException("Unable to parse datafile: " + json, e); @@ -348,5 +352,19 @@ private List parseLiveVariableInstances(JSONArray liv return liveVariableUsageInstances; } + + private List parseRollouts(JSONArray rolloutsJson) { + List rollouts = new ArrayList(rolloutsJson.size()); + + for (Object obj : rolloutsJson) { + JSONObject rolloutObject = (JSONObject) obj; + String id = (String) rolloutObject.get("id"); + List experiments = parseExperiments((JSONArray) rolloutObject.get("experiments")); + + rollouts.add(new Rollout(id, experiments)); + } + + return rollouts; + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java index 3f4df5210..c9718d851 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java @@ -29,6 +29,7 @@ import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; import com.optimizely.ab.config.audience.Audience; import java.lang.reflect.Type; @@ -80,9 +81,12 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa } List featureFlags = null; + List rollouts = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { Type featureFlagsType = new TypeToken>() {}.getType(); featureFlags = context.deserialize(jsonObject.getAsJsonArray("featureFlags"), featureFlagsType); + Type rolloutsType = new TypeToken>() {}.getType(); + rollouts = context.deserialize(jsonObject.get("rollouts").getAsJsonArray(), rolloutsType); } return new ProjectConfig( @@ -97,7 +101,8 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa experiments, featureFlags, groups, - liveVariables + liveVariables, + rollouts ); } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java index 04503c150..6ebd3c4ec 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java @@ -30,6 +30,7 @@ import com.optimizely.ab.config.Group; import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; import com.optimizely.ab.config.audience.Audience; import java.io.IOException; @@ -74,9 +75,12 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte } List featureFlags = null; + List rollouts = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = mapper.readValue(node.get("featureFlags").toString(), new TypeReference>() {}); + rollouts = mapper.readValue(node.get("rollouts").toString(), + new TypeReference>(){}); } return new ProjectConfig( @@ -91,7 +95,8 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte experiments, featureFlags, groups, - liveVariables + liveVariables, + rollouts ); } } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java index fa4a43a25..c072d79ee 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java @@ -457,6 +457,7 @@ public static void verifyProjectConfig(@CheckForNull ProjectConfig actual, @Nonn verifyFeatureFlags(actual.getFeatureFlags(), expected.getFeatureFlags()); verifyLiveVariables(actual.getLiveVariables(), expected.getLiveVariables()); verifyGroups(actual.getGroups(), expected.getGroups()); + verifyRollouts(actual.getRollouts(), expected.getRollouts()); } /** @@ -617,6 +618,23 @@ private static void verifyLiveVariables(List actual, List actual, List expected) { + if (expected == null) { + assertNull(actual); + } + else { + assertEquals(expected.size(), actual.size()); + + for (int i = 0; i < actual.size(); i++) { + Rollout actualRollout = actual.get(i); + Rollout expectedRollout = expected.get(i); + + assertEquals(expectedRollout.getId(), actualRollout.getId()); + verifyExperiments(actualRollout.getExperiments(), expectedRollout.getExperiments()); + } + } + } + /** * Verify that the provided variation-level live variable usage instances are equivalent. */ diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index e163abd52..b073b04d6 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -143,7 +143,7 @@ public class ValidProjectConfigV4 { private static final FeatureFlag FEATURE_FLAG_SINGLE_VARIABLE_STRING = new FeatureFlag( FEATURE_SINGLE_VARIABLE_STRING_ID, FEATURE_SINGLE_VARIABLE_STRING_KEY, - "", + "1058508303", Collections.emptyList(), Collections.singletonList( VARIABLE_STRING_VARIABLE @@ -667,6 +667,43 @@ public class ValidProjectConfigV4 { ) ); + private static final String ROLLOUT_1_ID = "1058508303"; + private static final String ROLLOUT_1_EVERYONE_ELSE_EXPERIMENT_ID = "1785077004"; + private static final String ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_ID = "1566407342"; + private static final String ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_STRING_VALUE = "lumos"; + private static final Variation ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION = new Variation( + ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_ID, + ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_ID, + Collections.singletonList( + new LiveVariableUsageInstance( + VARIABLE_STRING_VARIABLE_ID, + ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_STRING_VALUE + ) + ) + ); + private static final Experiment ROLLOUT_1_EVERYONE_ELSE_RULE = new Experiment( + ROLLOUT_1_EVERYONE_ELSE_EXPERIMENT_ID, + ROLLOUT_1_EVERYONE_ELSE_EXPERIMENT_ID, + Experiment.ExperimentStatus.RUNNING.toString(), + ROLLOUT_1_ID, + Collections.emptyList(), + Collections.singletonList( + ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION + ), + Collections.emptyMap(), + Collections.singletonList( + new TrafficAllocation( + ROLLOUT_1_EVERYONE_ELSE_RULE_ENABLED_VARIATION_ID, + 5000 + ) + ) + ); + private static final Rollout ROLLOUT_1 = new Rollout( + ROLLOUT_1_ID, + Collections.singletonList( + ROLLOUT_1_EVERYONE_ELSE_RULE + ) + ); public static ProjectConfig generateValidProjectConfigV4() { @@ -705,6 +742,10 @@ public static ProjectConfig generateValidProjectConfigV4() { groups.add(GROUP_1); groups.add(GROUP_2); + // list rollouts + List rollouts = new ArrayList(); + rollouts.add(ROLLOUT_1); + return new ProjectConfig( ACCOUNT_ID, ANONYMIZE_IP, @@ -717,7 +758,8 @@ public static ProjectConfig generateValidProjectConfigV4() { experiments, featureFlags, groups, - Collections.emptyList() + Collections.emptyList(), + rollouts ); } } diff --git a/core-api/src/test/resources/config/valid-project-config-v4.json b/core-api/src/test/resources/config/valid-project-config-v4.json index 165704d20..e56b804ed 100644 --- a/core-api/src/test/resources/config/valid-project-config-v4.json +++ b/core-api/src/test/resources/config/valid-project-config-v4.json @@ -423,7 +423,7 @@ { "id": "2079378557", "key": "string_single_variable_feature", - "rolloutId": "", + "rolloutId": "1058508303", "experimentIds": [], "variables": [ { @@ -469,5 +469,38 @@ ] } ], + "rollouts": [ + { + "id": "1058508303", + "experiments": [ + { + "id": "1785077004", + "key": "1785077004", + "status": "Running", + "layerId": "1058508303", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "1566407342", + "key": "1566407342", + "variables": [ + { + "id": "2077511132", + "value": "lumos" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1566407342", + "endOfRange": 5000 + } + ] + } + ] + } + ], "variables": [] }