Skip to content

Commit ad49a4a

Browse files
authored
feature flag parsing (#130)
* add variableIdToLiveVariableUsageInstanceMap to Varaitions. * Add equality methods to LiveVariable and FeatureFlag models to improve ProjectConfigParsing evaluation. *Update the v4 datafile with an empty top level "variables" array * Added empty array passed in to ProjectConfig constructor in ValidProjectConfigV4.java * ProjectConfigTestUtils now compare feature flags more effectively * Add GsonParser for FeatureFlags * Add org.Json FeatureFlag parsing * Add JsonSimple FeatureFlag parsing * add variableKeyToLiveVariableMap to FeatureFlag
1 parent f76fb96 commit ad49a4a

17 files changed

+382
-66
lines changed

core-api/src/main/java/com/optimizely/ab/config/FeatureFlag.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.fasterxml.jackson.annotation.JsonProperty;
2222

2323
import java.util.List;
24+
import java.util.Map;
2425

2526
/**
2627
* Represents a FeatureFlag definition at the project level
@@ -33,6 +34,7 @@ public class FeatureFlag implements IdKeyMapped{
3334
private final String layerId;
3435
private final List<String> experimentIds;
3536
private final List<LiveVariable> variables;
37+
private final Map<String, LiveVariable> variableKeyToLiveVariableMap;
3638

3739
@JsonCreator
3840
public FeatureFlag(@JsonProperty("id") String id,
@@ -45,6 +47,7 @@ public FeatureFlag(@JsonProperty("id") String id,
4547
this.layerId = layerId;
4648
this.experimentIds = experimentIds;
4749
this.variables = variables;
50+
this.variableKeyToLiveVariableMap = ProjectConfigUtils.generateNameMapping(variables);
4851
}
4952

5053
public String getId() {
@@ -67,6 +70,10 @@ public List<LiveVariable> getVariables() {
6770
return variables;
6871
}
6972

73+
public Map<String, LiveVariable> getVariableKeyToLiveVariableMap() {
74+
return variableKeyToLiveVariableMap;
75+
}
76+
7077
@Override
7178
public String toString() {
7279
return "FeatureFlag{" +
@@ -75,6 +82,33 @@ public String toString() {
7582
", layerId='" + layerId + '\'' +
7683
", experimentIds=" + experimentIds +
7784
", variables=" + variables +
85+
", variableKeyToLiveVariableMap=" + variableKeyToLiveVariableMap +
7886
'}';
7987
}
88+
89+
@Override
90+
public boolean equals(Object o) {
91+
if (this == o) return true;
92+
if (o == null || getClass() != o.getClass()) return false;
93+
94+
FeatureFlag that = (FeatureFlag) o;
95+
96+
if (!id.equals(that.id)) return false;
97+
if (!key.equals(that.key)) return false;
98+
if (!layerId.equals(that.layerId)) return false;
99+
if (!experimentIds.equals(that.experimentIds)) return false;
100+
if (!variables.equals(that.variables)) return false;
101+
return variableKeyToLiveVariableMap.equals(that.variableKeyToLiveVariableMap);
102+
}
103+
104+
@Override
105+
public int hashCode() {
106+
int result = id.hashCode();
107+
result = 31 * result + key.hashCode();
108+
result = 31 * result + layerId.hashCode();
109+
result = 31 * result + experimentIds.hashCode();
110+
result = 31 * result + variables.hashCode();
111+
result = 31 * result + variableKeyToLiveVariableMap.hashCode();
112+
return result;
113+
}
80114
}

core-api/src/main/java/com/optimizely/ab/config/LiveVariable.java

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.fasterxml.jackson.annotation.JsonValue;
2323
import com.google.gson.annotations.SerializedName;
2424

25+
import javax.annotation.Nullable;
26+
2527
/**
2628
* Represents a live variable definition at the project level
2729
*/
@@ -100,7 +102,7 @@ public static VariableType fromString(String variableTypeString) {
100102
private final String key;
101103
private final String defaultValue;
102104
private final VariableType type;
103-
private final VariableStatus status;
105+
@Nullable private final VariableStatus status;
104106

105107
@JsonCreator
106108
public LiveVariable(@JsonProperty("id") String id,
@@ -111,16 +113,11 @@ public LiveVariable(@JsonProperty("id") String id,
111113
this.id = id;
112114
this.key = key;
113115
this.defaultValue = defaultValue;
114-
if (status == null) {
115-
this.status = VariableStatus.ACTIVE;
116-
}
117-
else {
118-
this.status = status;
119-
}
116+
this.status = status;
120117
this.type = type;
121118
}
122119

123-
public VariableStatus getStatus() {
120+
public @Nullable VariableStatus getStatus() {
124121
return status;
125122
}
126123

@@ -150,4 +147,28 @@ public String toString() {
150147
", status=" + status +
151148
'}';
152149
}
150+
151+
@Override
152+
public boolean equals(Object o) {
153+
if (this == o) return true;
154+
if (o == null || getClass() != o.getClass()) return false;
155+
156+
LiveVariable variable = (LiveVariable) o;
157+
158+
if (!id.equals(variable.id)) return false;
159+
if (!key.equals(variable.key)) return false;
160+
if (!defaultValue.equals(variable.defaultValue)) return false;
161+
if (type != variable.type) return false;
162+
return status == variable.status;
163+
}
164+
165+
@Override
166+
public int hashCode() {
167+
int result = id.hashCode();
168+
result = 31 * result + key.hashCode();
169+
result = 31 * result + defaultValue.hashCode();
170+
result = 31 * result + type.hashCode();
171+
result = 31 * result + status.hashCode();
172+
return result;
173+
}
153174
}

core-api/src/main/java/com/optimizely/ab/config/LiveVariableUsageInstance.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* Represents the value of a live variable for a variation
2525
*/
2626
@JsonIgnoreProperties(ignoreUnknown = true)
27-
public class LiveVariableUsageInstance {
27+
public class LiveVariableUsageInstance implements IdMapped {
2828

2929
private final String id;
3030
private final String value;

core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ public List<Experiment> getExperimentsForEventKey(String eventKey) {
224224
return Collections.emptyList();
225225
}
226226

227+
public List<FeatureFlag> getFeatureFlags() {
228+
return featureFlags;
229+
}
230+
227231
public List<Attribute> getAttributes() {
228232
return attributes;
229233
}

core-api/src/main/java/com/optimizely/ab/config/Variation.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import javax.annotation.Nonnull;
2424
import javax.annotation.Nullable;
2525

26+
import java.util.Collections;
2627
import java.util.List;
28+
import java.util.Map;
2729

2830
/**
2931
* Represents the Optimizely Variation configuration.
@@ -36,6 +38,7 @@ public class Variation implements IdKeyMapped {
3638
private final String id;
3739
private final String key;
3840
private final List<LiveVariableUsageInstance> liveVariableUsageInstances;
41+
private final Map<String, LiveVariableUsageInstance> variableIdToLiveVariableUsageInstanceMap;
3942

4043
public Variation(String id, String key) {
4144
this(id, key, null);
@@ -47,7 +50,13 @@ public Variation(@JsonProperty("id") String id,
4750
@JsonProperty("variables") List<LiveVariableUsageInstance> liveVariableUsageInstances) {
4851
this.id = id;
4952
this.key = key;
50-
this.liveVariableUsageInstances = liveVariableUsageInstances;
53+
if (liveVariableUsageInstances == null) {
54+
this.liveVariableUsageInstances = Collections.emptyList();
55+
}
56+
else {
57+
this.liveVariableUsageInstances = liveVariableUsageInstances;
58+
}
59+
this.variableIdToLiveVariableUsageInstanceMap = ProjectConfigUtils.generateIdMapping(this.liveVariableUsageInstances);
5160
}
5261

5362
public @Nonnull String getId() {
@@ -62,6 +71,10 @@ public Variation(@JsonProperty("id") String id,
6271
return liveVariableUsageInstances;
6372
}
6473

74+
public Map<String, LiveVariableUsageInstance> getVariableIdToLiveVariableUsageInstanceMap() {
75+
return variableIdToLiveVariableUsageInstanceMap;
76+
}
77+
6578
public boolean is(String otherKey) {
6679
return key.equals(otherKey);
6780
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
*
3+
* Copyright 2017, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.config.parser;
18+
19+
import com.google.gson.JsonDeserializationContext;
20+
import com.google.gson.JsonDeserializer;
21+
import com.google.gson.JsonElement;
22+
import com.google.gson.JsonObject;
23+
import com.google.gson.JsonParseException;
24+
import com.optimizely.ab.config.FeatureFlag;
25+
26+
import java.lang.reflect.Type;
27+
28+
public class FeatureFlagGsonDeserializer implements JsonDeserializer<FeatureFlag> {
29+
@Override
30+
public FeatureFlag deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
31+
throws JsonParseException {
32+
33+
JsonObject jsonObject = json.getAsJsonObject();
34+
return GsonHelpers.parseFeatureFlag(jsonObject, context);
35+
}
36+
}

core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919
import com.google.gson.Gson;
2020
import com.google.gson.GsonBuilder;
21-
2221
import com.optimizely.ab.config.Experiment;
22+
import com.optimizely.ab.config.FeatureFlag;
2323
import com.optimizely.ab.config.Group;
24-
import com.optimizely.ab.config.audience.Audience;
2524
import com.optimizely.ab.config.ProjectConfig;
25+
import com.optimizely.ab.config.audience.Audience;
2626

2727
import javax.annotation.Nonnull;
2828

@@ -40,11 +40,12 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
4040
throw new ConfigParseException("Unable to parse empty json.");
4141
}
4242
Gson gson = new GsonBuilder()
43-
.registerTypeAdapter(ProjectConfig.class, new ProjectConfigGsonDeserializer())
44-
.registerTypeAdapter(Audience.class, new AudienceGsonDeserializer())
45-
.registerTypeAdapter(Group.class, new GroupGsonDeserializer())
46-
.registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer())
47-
.create();
43+
.registerTypeAdapter(Audience.class, new AudienceGsonDeserializer())
44+
.registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer())
45+
.registerTypeAdapter(FeatureFlag.class, new FeatureFlagGsonDeserializer())
46+
.registerTypeAdapter(Group.class, new GroupGsonDeserializer())
47+
.registerTypeAdapter(ProjectConfig.class, new ProjectConfigGsonDeserializer())
48+
.create();
4849

4950
try {
5051
return gson.fromJson(json, ProjectConfig.class);

core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,30 @@
2020
import com.google.gson.JsonDeserializationContext;
2121
import com.google.gson.JsonElement;
2222
import com.google.gson.JsonObject;
23+
import com.google.gson.JsonParseException;
2324
import com.google.gson.reflect.TypeToken;
24-
25+
import com.optimizely.ab.bucketing.DecisionService;
2526
import com.optimizely.ab.config.Experiment;
2627
import com.optimizely.ab.config.Experiment.ExperimentStatus;
28+
import com.optimizely.ab.config.FeatureFlag;
29+
import com.optimizely.ab.config.LiveVariable;
2730
import com.optimizely.ab.config.LiveVariableUsageInstance;
2831
import com.optimizely.ab.config.TrafficAllocation;
2932
import com.optimizely.ab.config.Variation;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
3035

3136
import java.lang.reflect.Type;
3237
import java.util.ArrayList;
33-
import java.util.List;
3438
import java.util.HashMap;
39+
import java.util.List;
3540
import java.util.Map;
3641
import java.util.Set;
3742

3843
final class GsonHelpers {
3944

45+
private static final Logger logger = LoggerFactory.getLogger(DecisionService.class);
46+
4047
private static List<Variation> parseVariations(JsonArray variationJson, JsonDeserializationContext context) {
4148
List<Variation> variations = new ArrayList<Variation>(variationJson.size());
4249
for (Object obj : variationJson) {
@@ -114,4 +121,35 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso
114121
static Experiment parseExperiment(JsonObject experimentJson, JsonDeserializationContext context) {
115122
return parseExperiment(experimentJson, "", context);
116123
}
124+
125+
static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) {
126+
String id = featureFlagJson.get("id").getAsString();
127+
String key = featureFlagJson.get("key").getAsString();
128+
String layerId = featureFlagJson.get("layerId").getAsString();
129+
130+
JsonArray experimentIdsJson = featureFlagJson.getAsJsonArray("experimentIds");
131+
List<String> experimentIds = new ArrayList<String>();
132+
for (JsonElement experimentIdObj : experimentIdsJson) {
133+
experimentIds.add(experimentIdObj.getAsString());
134+
}
135+
136+
List<LiveVariable> liveVariables = new ArrayList<LiveVariable>();
137+
try {
138+
Type liveVariableType = new TypeToken<List<LiveVariable>>() {}.getType();
139+
liveVariables = context.deserialize(featureFlagJson.getAsJsonArray("variables"),
140+
liveVariableType);
141+
}
142+
catch (JsonParseException exception) {
143+
logger.warn("Unable to parse variables for feature \"" + key
144+
+ "\". JsonParseException: " + exception);
145+
}
146+
147+
return new FeatureFlag(
148+
id,
149+
key,
150+
layerId,
151+
experimentIds,
152+
liveVariables
153+
);
154+
}
117155
}

0 commit comments

Comments
 (0)