Skip to content
This repository was archived by the owner on Apr 5, 2023. It is now read-only.

Commit 7f47f68

Browse files
fix(audienceeval): Multiple fixes (optimizely#355)
* fixed json parsing issue for base condition when type is null in datafile. * Fix for Empty Audience conditions array deserialization. * Fixed Audience conditions json serialization issue for empty conditions array. * Testcases adde to check for audience conditions with single or multiple audience Ids and no Evaluation operator. * 1. Fixed and optimized Audience condition deserialisation for cases where leaf node can contain further conditions array. 2. Fixed cases where Audience should be evaluated as null and is transformed to false in the final state. 3. Implemented test-cases for Audience Evaluation to fix issues detected on full-stack. * Test-cases added for nil or empty user attributes for Not condition. * Test case for Evaluation of Not operator in Audience conditions with nil or empty attributes updated.
1 parent 092aa13 commit 7f47f68

File tree

11 files changed

+1878
-95
lines changed

11 files changed

+1878
-95
lines changed

OptimizelySDKCore/OptimizelySDKCore.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@
233233
C77958C6219BFBC800B4CA89 /* OPTLYNSObject+Validation.m in Sources */ = {isa = PBXBuildFile; fileRef = C77958C1219BFBA000B4CA89 /* OPTLYNSObject+Validation.m */; };
234234
C779881321CBC22A002AAEC8 /* OPTLYValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = C779881221CBC22A002AAEC8 /* OPTLYValidationTest.m */; };
235235
C779881421CBC22A002AAEC8 /* OPTLYValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = C779881221CBC22A002AAEC8 /* OPTLYValidationTest.m */; };
236+
C781DEA921E8A44400EF35EC /* audience_targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = C781DEA821E8A44400EF35EC /* audience_targeting.json */; };
237+
C781DEAA21E8A44400EF35EC /* audience_targeting.json in Resources */ = {isa = PBXBuildFile; fileRef = C781DEA821E8A44400EF35EC /* audience_targeting.json */; };
236238
C78F98B7219ADEA600808062 /* OPTLYAudienceBaseCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = C78F98B4219ADE9600808062 /* OPTLYAudienceBaseCondition.h */; settings = {ATTRIBUTES = (Public, ); }; };
237239
C78F98B8219ADEA700808062 /* OPTLYAudienceBaseCondition.h in Headers */ = {isa = PBXBuildFile; fileRef = C78F98B4219ADE9600808062 /* OPTLYAudienceBaseCondition.h */; settings = {ATTRIBUTES = (Public, ); }; };
238240
C78F98B9219ADEAB00808062 /* OPTLYAudienceBaseCondition.m in Sources */ = {isa = PBXBuildFile; fileRef = C78F98B5219ADE9600808062 /* OPTLYAudienceBaseCondition.m */; };
@@ -647,6 +649,7 @@
647649
C77958C0219BFBA000B4CA89 /* OPTLYNSObject+Validation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OPTLYNSObject+Validation.h"; sourceTree = "<group>"; };
648650
C77958C1219BFBA000B4CA89 /* OPTLYNSObject+Validation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OPTLYNSObject+Validation.m"; sourceTree = "<group>"; };
649651
C779881221CBC22A002AAEC8 /* OPTLYValidationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OPTLYValidationTest.m; sourceTree = "<group>"; };
652+
C781DEA821E8A44400EF35EC /* audience_targeting.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = audience_targeting.json; sourceTree = "<group>"; };
650653
C78F98B4219ADE9600808062 /* OPTLYAudienceBaseCondition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OPTLYAudienceBaseCondition.h; sourceTree = "<group>"; };
651654
C78F98B5219ADE9600808062 /* OPTLYAudienceBaseCondition.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OPTLYAudienceBaseCondition.m; sourceTree = "<group>"; };
652655
C7ACD4FD218C2E4A008EC52E /* typed_audience_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = typed_audience_datafile.json; sourceTree = "<group>"; };
@@ -1197,6 +1200,7 @@
11971200
EA2FAB941DC6FDFA00B1D81B /* TestData */ = {
11981201
isa = PBXGroup;
11991202
children = (
1203+
C781DEA821E8A44400EF35EC /* audience_targeting.json */,
12001204
C7ACD4FD218C2E4A008EC52E /* typed_audience_datafile.json */,
12011205
EA2FAB951DC6FDFA00B1D81B /* BucketerTestsDatafile.json */,
12021206
3E92800D1F26AD4700214C58 /* BucketerTestsDatafile2.json */,
@@ -1705,6 +1709,7 @@
17051709
isa = PBXResourcesBuildPhase;
17061710
buildActionMask = 2147483647;
17071711
files = (
1712+
C781DEA921E8A44400EF35EC /* audience_targeting.json in Resources */,
17081713
EA2FABCC1DC6FDFA00B1D81B /* optimizely_6372300739.json in Resources */,
17091714
EA2FABD81DC6FDFA00B1D81B /* optimizely_7519590183.json in Resources */,
17101715
EA2FABD51DC6FDFA00B1D81B /* test_data_50_experiments.json in Resources */,
@@ -1731,6 +1736,7 @@
17311736
isa = PBXResourcesBuildPhase;
17321737
buildActionMask = 2147483647;
17331738
files = (
1739+
C781DEAA21E8A44400EF35EC /* audience_targeting.json in Resources */,
17341740
3E92800E1F26AD4700214C58 /* BucketerTestsDatafile2.json in Resources */,
17351741
EA2FABCD1DC6FDFA00B1D81B /* optimizely_6372300739.json in Resources */,
17361742
EA2FABD91DC6FDFA00B1D81B /* optimizely_7519590183.json in Resources */,

OptimizelySDKCore/OptimizelySDKCore/OPTLYAudience.m

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,12 @@ - (void)setConditionsWithNSArray:(NSArray *)array {
4242
}
4343

4444
- (nullable NSNumber *)evaluateConditionsWithAttributes:(NSDictionary<NSString *, NSObject *> *)attributes projectConfig:(nullable OPTLYProjectConfig *)config {
45-
for (NSObject<OPTLYCondition> *condition in self.conditions) {
46-
NSNumber *result = [condition evaluateConditionsWithAttributes:attributes projectConfig:config];
47-
if (result != NULL && [result boolValue] == true) {
48-
// if user satisfies any conditions, return true.
49-
return [NSNumber numberWithBool:true];
50-
}
45+
46+
NSObject<OPTLYCondition> *condition = (NSObject<OPTLYCondition> *)[self.conditions firstObject];
47+
if (condition) {
48+
return [condition evaluateConditionsWithAttributes:attributes projectConfig:config];
5149
}
52-
// if user doesn't satisfy any conditions, return false.
53-
return [NSNumber numberWithBool:false];
50+
return nil;
5451
}
5552

5653
@end

OptimizelySDKCore/OptimizelySDKCore/OPTLYAudienceBaseCondition.m

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,7 @@ - (nullable NSNumber *)evaluateConditionsWithAttributes:(NSDictionary<NSString *
3030
}
3131

3232
OPTLYAudience *audience = [config getAudienceForId:self.audienceId];
33-
BOOL areAttributesValid = [[audience evaluateConditionsWithAttributes:attributes projectConfig:config] boolValue];
34-
if (areAttributesValid) {
35-
return [NSNumber numberWithBool:true];;
36-
}
37-
38-
return [NSNumber numberWithBool:false];
33+
return [audience evaluateConditionsWithAttributes:attributes projectConfig:config];
3934
}
4035

4136
@end

OptimizelySDKCore/OptimizelySDKCore/OPTLYBaseCondition.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
/// Condition name
3535
@property (nonatomic, strong) NSString *name;
3636
/// Condition type
37-
@property (nonatomic, strong) NSString *type;
37+
@property (nonatomic, strong, nullable) NSString<OPTLYOptional> *type;
3838
/// Condition value
3939
@property (nonatomic, strong, nullable) NSObject<OPTLYOptional> *value;
4040
/// Condition match type

OptimizelySDKCore/OptimizelySDKCore/OPTLYBaseCondition.m

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,7 @@ + (OPTLYJSONKeyMapper*)keyMapper
3333
}
3434

3535
+ (BOOL) isBaseConditionJSON:(NSData *)jsonData {
36-
if (![jsonData isKindOfClass:[NSDictionary class]]) {
37-
return false;
38-
}
39-
else {
40-
NSDictionary *dict = (NSDictionary *)jsonData;
41-
42-
if (dict[OPTLYDatafileKeysConditionName] != nil &&
43-
dict[OPTLYDatafileKeysConditionType] != nil) {
44-
return true;
45-
}
46-
return false;
47-
}
36+
return [jsonData isKindOfClass:[NSDictionary class]];
4837
}
4938

5039
-(nullable NSNumber *)evaluateMatchTypeExact:(NSDictionary<NSString *, NSObject *> *)attributes{
@@ -116,15 +105,15 @@ -(nullable NSNumber *)evaluateCustomMatchType:(NSDictionary<NSString *, NSObject
116105
//Check if given type is the required type
117106
return NULL;
118107
}
119-
else if (!self.match || [self.match isEqualToString:@""]){
120-
//Check if given match is empty, if so, opt for legacy Exact Matching
121-
self.match = OPTLYDatafileKeysMatchTypeExact;
122-
}
123108
else if (self.value == NULL && ![self.match isEqualToString:OPTLYDatafileKeysMatchTypeExists]){
124109
//Check if given value is null, which is only acceptable if match type is Exists
125110
return NULL;
126111
}
127-
112+
if (!self.match || [self.match isEqualToString:@""]){
113+
//Check if given match is empty, if so, opt for legacy Exact Matching
114+
self.match = OPTLYDatafileKeysMatchTypeExact;
115+
}
116+
128117
SWITCH(self.match){
129118
CASE(OPTLYDatafileKeysMatchTypeExact) {
130119
return [self evaluateMatchTypeExact: attributes];

OptimizelySDKCore/OptimizelySDKCore/OPTLYCondition.m

Lines changed: 42 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ @implementation OPTLYCondition
3939

4040
// need to check if the jsonArray is actually an array, otherwise, something is wrong with the audience condition
4141
if (![jsonArray isKindOfClass:[NSArray class]]) {
42-
if ([jsonArray isKindOfClass:[NSDictionary class]] && [OPTLYBaseCondition isBaseConditionJSON:((NSData *)jsonArray)]) {
42+
if ([OPTLYBaseCondition isBaseConditionJSON:((NSData *)jsonArray)]) {
4343
mutableJsonArray = [[NSMutableArray alloc] initWithArray:@[OPTLYDatafileKeysOrCondition,jsonArray]];
4444
}
4545
else {
@@ -61,11 +61,10 @@ @implementation OPTLYCondition
6161
[mutableJsonArray insertObject:OPTLYDatafileKeysOrCondition atIndex:0];
6262
}
6363

64-
if ([OPTLYBaseCondition isBaseConditionJSON:mutableJsonArray[1]]) { //base case condition
65-
66-
// generate all base conditions
67-
NSMutableArray<OPTLYCondition> *conditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(mutableJsonArray.count - 1)];
68-
for (int i = 1; i < mutableJsonArray.count; i++) {
64+
NSMutableArray<OPTLYCondition> *conditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(mutableJsonArray.count - 1)];
65+
for (int i = 1; i < mutableJsonArray.count; i++) {
66+
if ([OPTLYBaseCondition isBaseConditionJSON: mutableJsonArray[i]]) {
67+
// generate base condition
6968
NSDictionary *info = mutableJsonArray[i];
7069
NSError *err = nil;
7170
OPTLYBaseCondition *condition = [[OPTLYBaseCondition alloc] initWithDictionary:info
@@ -78,41 +77,33 @@ @implementation OPTLYCondition
7877
[conditions addObject:condition];
7978
}
8079
}
80+
continue;
8181
}
8282

83-
// return an (And/Or/Not) Condition handling the base conditions
84-
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:mutableJsonArray[0]
85-
withConditions:conditions];
86-
return (NSArray<OPTLYCondition *><OPTLYCondition> *)@[condition];
87-
}
88-
else {
89-
9083
// further condition arrays to deserialize
91-
NSMutableArray<OPTLYCondition> *subConditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(mutableJsonArray.count - 1)];
92-
for (int i = 1; i < mutableJsonArray.count; i++) {
93-
NSError *err = nil;
94-
NSArray *deserializedJsonObject = [OPTLYCondition deserializeJSONArray:mutableJsonArray[i] error:&err];
95-
96-
if (err) {
97-
*error = err;
98-
return nil;
99-
}
100-
101-
if (deserializedJsonObject != nil) {
102-
[subConditions addObjectsFromArray:deserializedJsonObject];
103-
}
84+
NSError *err = nil;
85+
NSArray *deserializedJsonObject = [OPTLYCondition deserializeJSONArray:mutableJsonArray[i] error:&err];
86+
if (err) {
87+
*error = err;
88+
return nil;
89+
}
90+
if (deserializedJsonObject != nil) {
91+
[conditions addObjectsFromArray:deserializedJsonObject];
10492
}
105-
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:mutableJsonArray[0]
106-
withConditions:subConditions];
107-
return (NSArray<OPTLYCondition *><OPTLYCondition> *)@[condition];
10893
}
94+
95+
// return an (And/Or/Not) Condition handling the base conditions
96+
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:mutableJsonArray[0]
97+
withConditions:conditions];
98+
return (NSArray<OPTLYCondition *><OPTLYCondition> *)@[condition];
10999
}
110100

111101
// example jsonArray:
112102
// "[\"and\", [\"or\", \"3468206642\", \"3988293898\"], [\"or\", \"3988293899\", \"3468206646\", \"3468206647\", \"3468206644\", \"3468206643\"]]"
113103
+ (NSArray<OPTLYCondition> *)deserializeAudienceConditionsJSONArray:(NSArray *)jsonArray
114104
error:(NSError * __autoreleasing *)error {
115105

106+
NSMutableArray *mutableJsonArray = [NSMutableArray new];
116107
// need to check if the jsonArray is actually an array, otherwise, something is wrong with the audience condition
117108
if (![jsonArray isKindOfClass:[NSArray class]]) {
118109
NSError *err = [NSError errorWithDomain:OPTLYErrorHandlerMessagesDomain
@@ -123,44 +114,42 @@ @implementation OPTLYCondition
123114
}
124115
return nil;
125116
}
117+
if ([jsonArray count] == 0) {
118+
return (NSArray<OPTLYCondition> *)@[];
119+
}
126120

127-
if ([OPTLYAudienceBaseCondition isBaseConditionJSON:jsonArray[1]]) { //base case condition
128-
129-
// generate all base conditions
130-
NSMutableArray<OPTLYCondition> *conditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(jsonArray.count - 1)];
131-
for (int i = 1; i < jsonArray.count; i++) {
132-
NSString *audienceId = jsonArray[i];
121+
mutableJsonArray = [jsonArray mutableCopy];
122+
// Should return 'OR' operator in case there is none
123+
if (![mutableJsonArray[0] isEqualToString:OPTLYDatafileKeysAndCondition] && ![mutableJsonArray[0] isEqualToString:OPTLYDatafileKeysOrCondition] && ![mutableJsonArray[0] isEqualToString:OPTLYDatafileKeysNotCondition]) {
124+
[mutableJsonArray insertObject:OPTLYDatafileKeysOrCondition atIndex:0];
125+
}
126+
127+
NSMutableArray<OPTLYCondition> *conditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(mutableJsonArray.count - 1)];
128+
for (int i = 1; i < mutableJsonArray.count; i++) {
129+
if ([OPTLYAudienceBaseCondition isBaseConditionJSON: mutableJsonArray[i]]) {
130+
// generate base condition
131+
NSString *audienceId = mutableJsonArray[i];
133132
OPTLYAudienceBaseCondition *condition = [OPTLYAudienceBaseCondition new];
134133
condition.audienceId = audienceId;
135134
[conditions addObject:condition];
135+
continue;
136136
}
137137

138-
// return an (And/Or/Not) Condition handling the base conditions
139-
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:jsonArray[0]
140-
withConditions:conditions];
141-
return (NSArray<OPTLYCondition> *)@[condition];
142-
}
143-
else {
144-
145138
// further condition arrays to deserialize
146-
NSMutableArray<OPTLYCondition> *subConditions = (NSMutableArray<OPTLYCondition> *)[[NSMutableArray alloc] initWithCapacity:(jsonArray.count - 1)];
147-
for (int i = 1; i < jsonArray.count; i++) {
139+
if ([mutableJsonArray[i] isKindOfClass:[NSArray class]]) {
148140
NSError *err = nil;
149-
NSArray *deserializedJsonObject = [OPTLYCondition deserializeAudienceConditionsJSONArray:jsonArray[i] error:&err];
150-
141+
NSArray *_tmpConditions = [OPTLYCondition deserializeAudienceConditionsJSONArray:(NSArray *)mutableJsonArray[i] error:&err];
142+
[conditions addObject:[_tmpConditions firstObject]];
151143
if (err) {
152144
*error = err;
153145
return nil;
154146
}
155-
156-
if (deserializedJsonObject != nil) {
157-
[subConditions addObjectsFromArray:deserializedJsonObject];
158-
}
159147
}
160-
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:jsonArray[0]
161-
withConditions:subConditions];
162-
return (NSArray<OPTLYCondition> *)@[condition];
163148
}
149+
// return an (And/Or/Not) Condition handling the base conditions
150+
NSObject<OPTLYCondition> *condition = [OPTLYCondition createConditionInstanceOfClass:mutableJsonArray[0]
151+
withConditions:conditions];
152+
return (NSArray<OPTLYCondition> *)@[condition];
164153
}
165154

166155
+ (NSObject<OPTLYCondition> *)createConditionInstanceOfClass:(NSString *)conditionClass withConditions:(NSArray<OPTLYCondition> *)conditions {

OptimizelySDKCore/OptimizelySDKCore/OPTLYExperiment.m

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,11 @@ - (void)setAudienceConditionsWithNSArray:(NSArray *)array {
5757

5858
- (nullable NSNumber *)evaluateConditionsWithAttributes:(NSDictionary<NSString *, NSObject *> *)attributes projectConfig:(nullable OPTLYProjectConfig *)config {
5959

60-
for (NSObject<OPTLYCondition> *condition in self.audienceConditions) {
61-
NSNumber *result = [condition evaluateConditionsWithAttributes:attributes projectConfig:config];
62-
if (result != NULL && [result boolValue] == true) {
63-
// if user satisfies any conditions, return true.
64-
return [NSNumber numberWithBool:true];
65-
}
60+
NSObject<OPTLYCondition> *condition = (NSObject<OPTLYCondition> *)[self.audienceConditions firstObject];
61+
if (condition) {
62+
return [condition evaluateConditionsWithAttributes:attributes projectConfig:config];
6663
}
67-
// if user doesn't satisfy any conditions, return false.
68-
return [NSNumber numberWithBool:false];
64+
return nil;
6965
}
7066

7167
- (void)setGroupId:(NSString *)groupId {

0 commit comments

Comments
 (0)