Skip to content

Commit bf59159

Browse files
patch user attribute evaluation (#152)
* patch evaluation by using equals * make value optional in the UserAttribute class. Add malformed audience and experiment using it in the parser * add test for null value evaluation. add audience to experiment * add test for evaluating audience with missing condition value
1 parent 466ebaa commit bf59159

File tree

6 files changed

+64
-25
lines changed

6 files changed

+64
-25
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19-
import java.util.List;
20-
import java.util.Map;
21-
2219
import javax.annotation.Nonnull;
2320
import javax.annotation.concurrent.Immutable;
21+
import java.util.List;
22+
import java.util.Map;
2423

2524
/**
2625
* Represents an 'And' conditions condition operation.
@@ -29,7 +28,6 @@
2928
public class AndCondition implements Condition {
3029

3130
private final List<Condition> conditions;
32-
3331
public AndCondition(@Nonnull List<Condition> conditions) {
3432
this.conditions = conditions;
3533
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import javax.annotation.Nonnull;
20+
import javax.annotation.concurrent.Immutable;
1921
import java.util.List;
2022
import java.util.Map;
2123

22-
import javax.annotation.concurrent.Immutable;
23-
import javax.annotation.Nonnull;
24-
2524
/**
2625
* Represents an 'Or' conditions condition operation.
2726
*/
2827
@Immutable
2928
public class OrCondition implements Condition {
30-
3129
private final List<Condition> conditions;
3230

3331
public OrCondition(@Nonnull List<Condition> conditions) {

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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.optimizely.ab.config.audience;
1818

1919
import javax.annotation.Nonnull;
20+
import javax.annotation.Nullable;
2021
import javax.annotation.concurrent.Immutable;
2122
import java.util.Map;
2223

@@ -30,7 +31,7 @@ public class UserAttribute implements Condition {
3031
private final String type;
3132
private final String value;
3233

33-
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nonnull String value) {
34+
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable String value) {
3435
this.name = name;
3536
this.type = type;
3637
this.value = value;
@@ -51,7 +52,17 @@ public String getValue() {
5152
public boolean evaluate(Map<String, String> attributes) {
5253
String userAttributeValue = attributes.get(name);
5354

54-
return value.equals(userAttributeValue);
55+
if (value != null) { // if there is a value in the condition
56+
// check user attribute value is equal
57+
return value.equals(userAttributeValue);
58+
}
59+
else if (userAttributeValue != null) { // if the datafile value is null but user has a value for this attribute
60+
// return false since null != nonnull
61+
return false;
62+
}
63+
else { // both are null
64+
return true;
65+
}
5566
}
5667

5768
@Override
@@ -63,23 +74,22 @@ public String toString() {
6374
}
6475

6576
@Override
66-
public boolean equals(Object other) {
67-
if (!(other instanceof UserAttribute))
68-
return false;
77+
public boolean equals(Object o) {
78+
if (this == o) return true;
79+
if (o == null || getClass() != o.getClass()) return false;
6980

70-
UserAttribute otherConditionObj = (UserAttribute)other;
81+
UserAttribute that = (UserAttribute) o;
7182

72-
return name.equals(otherConditionObj.getName()) && type.equals(otherConditionObj.getType())
73-
&& value.equals(otherConditionObj.getValue());
83+
if (!name.equals(that.name)) return false;
84+
if (!type.equals(that.type)) return false;
85+
return value != null ? value.equals(that.value) : that.value == null;
7486
}
7587

7688
@Override
7789
public int hashCode() {
78-
final int prime = 31;
79-
int result = 1;
80-
result = prime * result + name.hashCode();
81-
result = prime * result + type.hashCode();
82-
result = prime * result + value.hashCode();
90+
int result = name.hashCode();
91+
result = 31 * result + type.hashCode();
92+
result = 31 * result + (value != null ? value.hashCode() : 0);
8393
return result;
8494
}
8595
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,15 @@ private Condition parseConditions(JSONArray conditionJson) {
234234
conditions.add(parseConditions(conditionJson.getJSONArray(i)));
235235
} else {
236236
JSONObject conditionMap = (JSONObject)obj;
237-
conditions.add(new UserAttribute((String)conditionMap.get("name"), (String)conditionMap.get("type"),
238-
(String)conditionMap.get("value")));
237+
String value = null;
238+
if (conditionMap.has("value")) {
239+
value = conditionMap.getString("value");
240+
}
241+
conditions.add(new UserAttribute(
242+
(String)conditionMap.get("name"),
243+
(String)conditionMap.get("type"),
244+
value
245+
));
239246
}
240247
}
241248

core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
*/
1717
package com.optimizely.ab.config.audience;
1818

19+
import com.optimizely.ab.config.Experiment;
1920
import org.junit.Before;
2021
import org.junit.Test;
2122

2223
import java.util.ArrayList;
24+
import java.util.Collections;
2325
import java.util.HashMap;
2426
import java.util.List;
2527
import java.util.Map;
@@ -187,4 +189,28 @@ public void andConditionEvaluateFalse() throws Exception {
187189
// shouldn't be called due to short-circuiting in 'And' evaluation
188190
verify(orCondition2, times(0)).evaluate(testUserAttributes);
189191
}
192+
193+
/**
194+
* Verify that {@link UserAttribute#evaluate(Map)}
195+
* called when its attribute value is null
196+
* returns True when the user's attribute value is also null
197+
* True when the attribute is not in the map
198+
* False when empty string is used.
199+
* @throws Exception
200+
*/
201+
@Test
202+
public void nullValueEvaluate() throws Exception {
203+
String attributeName = "attribute_name";
204+
String attributeType = "attribute_type";
205+
String attributeValue = null;
206+
UserAttribute nullValueAttribute = new UserAttribute(
207+
attributeName,
208+
attributeType,
209+
attributeValue
210+
);
211+
212+
assertTrue(nullValueAttribute.evaluate(Collections.<String, String>emptyMap()));
213+
assertTrue(nullValueAttribute.evaluate(Collections.singletonMap(attributeName, attributeValue)));
214+
assertFalse(nullValueAttribute.evaluate((Collections.singletonMap(attributeName, ""))));
215+
}
190216
}

core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
*/
1717
package com.optimizely.ab.internal;
1818

19-
import com.optimizely.ab.config.audience.Audience;
20-
import com.optimizely.ab.config.audience.Condition;
2119
import com.optimizely.ab.config.Experiment;
2220
import com.optimizely.ab.config.Experiment.ExperimentStatus;
2321
import com.optimizely.ab.config.ProjectConfig;
2422
import com.optimizely.ab.config.TrafficAllocation;
2523
import com.optimizely.ab.config.Variation;
24+
import com.optimizely.ab.config.audience.Audience;
25+
import com.optimizely.ab.config.audience.Condition;
2626
import org.junit.BeforeClass;
2727
import org.junit.Test;
2828

0 commit comments

Comments
 (0)