Skip to content

Commit a5cdf42

Browse files
[FSSDK-11036] fix: event tags support nested objects (#570)
* fix: nested event tag support added * fix: update simulator matrix
1 parent b3651f6 commit a5cdf42

File tree

7 files changed

+147
-39
lines changed

7 files changed

+147
-39
lines changed

.github/workflows/swift.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626

2727
integration_tests:
2828
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
29-
uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@master
29+
uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@muzahid/fix-nested-event-tag
3030
secrets:
3131
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
3232
TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
@@ -47,7 +47,7 @@ jobs:
4747
4848
unittests:
4949
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
50-
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master
50+
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@muzahid/fix-nested-event-tag
5151

5252
prepare_for_release:
5353
runs-on: macos-13

.github/workflows/unit_tests.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,27 @@ jobs:
1919
# - see "https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md" for installed macOS, xcode and simulator versions.
2020
include:
2121
- os: 16.1
22-
device: "iPhone 12"
22+
device: "iPhone 14"
2323
scheme: "OptimizelySwiftSDK-iOS"
2424
test_sdk: "iphonesimulator"
2525
platform: "iOS Simulator"
2626
os_type: "iOS"
2727
simulator_xcode_version: 14.1
28-
- os: 15.5
29-
device: "iPhone 12"
28+
- os: 16.2
29+
device: "iPhone 14"
3030
scheme: "OptimizelySwiftSDK-iOS"
3131
test_sdk: "iphonesimulator"
3232
platform: "iOS Simulator"
3333
os_type: "iOS"
34-
simulator_xcode_version: 13.4.1
35-
- os: 15.5
34+
simulator_xcode_version: 14.2
35+
- os: 16.4
3636
# good to have tests with older OS versions, but it looks like this is min OS+xcode versions supported by github actions
37-
device: "iPad Air (4th generation)"
37+
device: "iPad Air (5th generation)"
3838
scheme: "OptimizelySwiftSDK-iOS"
3939
test_sdk: "iphonesimulator"
4040
platform: "iOS Simulator"
4141
os_type: "iOS"
42-
simulator_xcode_version: 13.4.1
42+
simulator_xcode_version: 14.3.1
4343
- os: 16.1
4444
device: "Apple TV"
4545
scheme: "OptimizelySwiftSDK-tvOS"
@@ -85,7 +85,7 @@ jobs:
8585
# - to find pre-installed xcode version, run this:
8686
##ls /Applications/
8787
# - to find supported simulator os versions, run this (and find simulator with non-error "datapath")
88-
##xcrun simctl list --json devices
88+
xcrun simctl list --json devices
8989
9090
# switch to the target xcode version
9191
sudo xcode-select -switch /Applications/Xcode_$SIMULATOR_XCODE_VERSION.app

Sources/Data Model/Audience/AttributeValue.swift

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
import Foundation
1818

1919
enum AttributeValue: Codable, Equatable, CustomStringConvertible {
20+
typealias AttrArray = Array<AttributeValue>
21+
typealias AttrDictionary = [String : AttributeValue]
22+
2023
case string(String)
2124
case int(Int64) // supported value range [-2^53, 2^53]
2225
case double(Double)
2326
case bool(Bool)
24-
// not defined in datafile schema, but required for forward compatiblity (see Nikhil's doc)
27+
case array(AttrArray)
28+
case dictionary(AttrDictionary)
2529
case others
2630

2731
var description: String {
@@ -34,6 +38,10 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
3438
return "int(\(value))"
3539
case .bool(let value):
3640
return "bool(\(value))"
41+
case .array(let value):
42+
return "array(\(value))"
43+
case .dictionary(let value):
44+
return "dictionary(\(value))"
3745
case .others:
3846
return "others"
3947
}
@@ -63,6 +71,18 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
6371
self = .bool(boolValue)
6472
return
6573
}
74+
75+
if let arrValue = value as? [Any] {
76+
let attr = arrValue.compactMap { AttributeValue(value: $0) }
77+
self = .array(attr)
78+
return
79+
}
80+
81+
if let dicValue = value as? [String : Any] {
82+
let attr = dicValue.compactMapValues { AttributeValue(value: $0) }
83+
self = .dictionary(attr)
84+
return
85+
}
6686

6787
return nil
6888
}
@@ -87,7 +107,18 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
87107
return
88108
}
89109

90-
// accept all other types (null, {}, []) for forward compatibility support
110+
if let value = try? container.decode(AttrArray.self) {
111+
self = .array(value)
112+
return
113+
}
114+
115+
if let value = try? container.decode(AttrDictionary.self) {
116+
self = .dictionary(value)
117+
return
118+
}
119+
120+
121+
// accept all other types (null) for forward compatibility support
91122
self = .others
92123
}
93124

@@ -103,6 +134,10 @@ enum AttributeValue: Codable, Equatable, CustomStringConvertible {
103134
try container.encode(value)
104135
case .bool(let value):
105136
try container.encode(value)
137+
case .array(let value):
138+
try container.encode(value)
139+
case .dictionary(let value):
140+
try container.encode(value.mapValues { $0 })
106141
case .others:
107142
return
108143
}
@@ -135,6 +170,14 @@ extension AttributeValue {
135170
return true
136171
}
137172

173+
if case .array(let selfArr) = self, case .array(let targetArr) = targetValue {
174+
return selfArr == targetArr
175+
}
176+
177+
if case .dictionary(let selfDict) = self, case .dictionary(let targetDict) = targetValue {
178+
return selfDict == targetDict
179+
}
180+
138181
return false
139182
}
140183

@@ -227,6 +270,10 @@ extension AttributeValue {
227270
return String(value)
228271
case .bool(let value):
229272
return String(value)
273+
case .array(let value):
274+
return String(describing: value)
275+
case .dictionary(let value):
276+
return String(describing: value)
230277
case .others:
231278
return "UNKNOWN"
232279
}
@@ -240,6 +287,8 @@ extension AttributeValue {
240287
case (.double, .int): return true
241288
case (.double, .double): return true
242289
case (.bool, .bool): return true
290+
case (.array, .array): return true
291+
case (.dictionary, .dictionary): return true
243292
default: return false
244293
}
245294
}
@@ -271,6 +320,8 @@ extension AttributeValue {
271320
case (.int): return true
272321
case (.double): return true
273322
case (.bool): return true
323+
case (.array): return true
324+
case (.dictionary): return true
274325
default: return false
275326
}
276327
}

Tests/OptimizelyTests-Common/BatchEventBuilderTests_EventTags.swift

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class BatchEventBuilderTests_EventTags: XCTestCase {
7676

7777
extension BatchEventBuilderTests_EventTags {
7878

79-
func testEventTagsWhenInvalidType() {
79+
func testEventTagsWhenArrayType() {
8080
let eventKey = "event_single_targeted_exp"
8181
let eventTags: [String: Any] = ["browser": "chrome",
8282
"future": [1, 2, 3]]
@@ -87,7 +87,8 @@ extension BatchEventBuilderTests_EventTags {
8787
let tags = de["tags"] as! [String: Any]
8888

8989
XCTAssertEqual(tags["browser"] as! String, "chrome")
90-
XCTAssertNil(tags["future"])
90+
XCTAssertNotNil(tags["future"])
91+
XCTAssertEqual(tags["future"] as? [Int], [1, 2, 3])
9192
}
9293

9394
func testEventTagsWhenTooBigNumbers() {
@@ -316,6 +317,55 @@ extension BatchEventBuilderTests_EventTags {
316317
XCTAssertEqual(de["value"] as! Double, 32, "value must be valid for value")
317318
}
318319

320+
321+
func testNestedTag() {
322+
let properties: [String: Any] = [
323+
"category": "shoes",
324+
"Text": "value",
325+
"nested": [
326+
"foot": "value",
327+
"mouth": "mouth_value"
328+
],
329+
"stringArray": ["a", "b", "c"],
330+
"intArray": [1, 2, 3],
331+
"doubleArray": [1.0, 2.0, 3.0],
332+
"boolAray": [false, true, false, true],
333+
]
334+
let eventKey = "event_single_targeted_exp"
335+
let eventTags: [String: Any] = ["browser": "chrome",
336+
"v1": Int8(10),
337+
"v2": Int16(20),
338+
"v3": Int32(30),
339+
"revenue": Int64(40),
340+
"value": Float(32),
341+
"$opt_event_properties": properties]
342+
343+
try! optimizely.track(eventKey: eventKey, userId: userId, attributes: nil, eventTags: eventTags)
344+
345+
let de = getDispatchEvent(dispatcher: eventDispatcher)!
346+
let tags = de["tags"] as! [String: Any]
347+
348+
XCTAssertEqual(tags["browser"] as! String, "chrome")
349+
XCTAssertEqual(tags["v1"] as! Int, 10)
350+
XCTAssertEqual(tags["v2"] as! Int, 20)
351+
XCTAssertEqual(tags["v3"] as! Int, 30)
352+
XCTAssertEqual(tags["revenue"] as! Int, 40)
353+
XCTAssertEqual(tags["value"] as! Double, 32)
354+
XCTAssertEqual(de["revenue"] as! Int, 40, "value must be valid for revenue")
355+
XCTAssertEqual(de["value"] as! Double, 32, "value must be valid for value")
356+
357+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["category"] as! String, "shoes")
358+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["nested"] as! [String : String], ["foot": "value", "mouth": "mouth_value"])
359+
360+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["stringArray"] as! [String], ["a", "b", "c"])
361+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["intArray"] as! [Int], [1, 2, 3])
362+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["doubleArray"] as! [Double], [1, 2, 3])
363+
XCTAssertEqual((tags["$opt_event_properties"] as! [String : Any])["boolAray"] as! [Bool], [false, true, false, true])
364+
365+
366+
}
367+
368+
319369
func testEventTagsWithRevenueAndValue_toJSON() {
320370

321371
// valid revenue/value types

Tests/OptimizelyTests-Common/DecisionServiceTests_Experiments.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,17 @@ class DecisionServiceTests_Experiments: XCTestCase {
158158
],
159159
[
160160
"id": kAudienceIdExactInvalidValue,
161-
"conditions": [ "type": "custom_attribute", "name": "age", "match": "exact", "value": ["invalid"] ],
161+
"conditions": [ "type": "custom_attribute", "name": "age", "match": "exact", "value": ["invalid" : nil] ],
162162
"name": "age"
163163
],
164164
[
165165
"id": kAudienceIdGtInvalidValue,
166-
"conditions": [ "type": "custom_attribute", "name": "age", "match": "gt", "value": ["invalid"] ],
166+
"conditions": [ "type": "custom_attribute", "name": "age", "match": "gt", "value": ["invalid" : nil] ],
167167
"name": "age"
168168
],
169169
[
170170
"id": kAudienceIdLtInvalidValue,
171-
"conditions": [ "type": "custom_attribute", "name": "age", "match": "lt", "value": ["invalid"] ],
171+
"conditions": [ "type": "custom_attribute", "name": "age", "match": "lt", "value": ["invalid" : nil] ],
172172
"name": "age"
173173
],
174174
[
@@ -565,7 +565,7 @@ extension DecisionServiceTests_Experiments {
565565
}
566566

567567
func testDoesMeetAudienceConditionsWithExactMatchAndInvalidValue() {
568-
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"exact\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
568+
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"exact\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
569569
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)
570570

571571
experiment = try! OTUtils.model(from: sampleExperimentData)
@@ -575,8 +575,6 @@ extension DecisionServiceTests_Experiments {
575575
result = self.decisionService.doesMeetAudienceConditions(config: config,
576576
experiment: experiment,
577577
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result
578-
579-
XCTAssert(MockLogger.logFound)
580578
XCTAssertFalse(result)
581579
}
582580

@@ -613,7 +611,7 @@ extension DecisionServiceTests_Experiments {
613611
}
614612

615613
func testDoesMeetAudienceConditionsWithGreaterMatchAndInvalidValue() {
616-
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"gt\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
614+
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"gt\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
617615
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)
618616

619617
experiment = try! OTUtils.model(from: sampleExperimentData)
@@ -623,7 +621,6 @@ extension DecisionServiceTests_Experiments {
623621
result = self.decisionService.doesMeetAudienceConditions(config: config,
624622
experiment: experiment,
625623
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result
626-
627624
XCTAssert(MockLogger.logFound)
628625
XCTAssertFalse(result)
629626
}
@@ -645,7 +642,7 @@ extension DecisionServiceTests_Experiments {
645642
}
646643

647644
func testDoesMeetAudienceConditionsWithLessMatchAndInvalidValue() {
648-
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"lt\",\"value\":{},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
645+
MockLogger.expectedLog = OptimizelyError.evaluateAttributeInvalidCondition("{\"match\":\"lt\",\"value\":{\"invalid\":{}},\"name\":\"age\",\"type\":\"custom_attribute\"}").localizedDescription
649646
self.config.project.typedAudiences = try! OTUtils.model(from: sampleTypedAudiencesData)
650647

651648
experiment = try! OTUtils.model(from: sampleExperimentData)
@@ -655,7 +652,6 @@ extension DecisionServiceTests_Experiments {
655652
result = self.decisionService.doesMeetAudienceConditions(config: config,
656653
experiment: experiment,
657654
user: OTUtils.user(userId: kUserId, attributes: kAttributesAgeMatch)).result
658-
659655
XCTAssert(MockLogger.logFound)
660656
XCTAssertFalse(result)
661657
}

Tests/OptimizelyTests-DataModel/AttributeValueTests.swift

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,27 @@ class AttributeValueTests: XCTestCase {
128128
XCTAssert(model2 == AttributeValue.int(Int64(value)))
129129
}
130130

131-
func testDecodeSuccessWithInvalidType() {
132-
let value = ["invalid type"]
131+
func testDecodeSuccessWithArrayType() {
132+
let value = ["array type"]
133133

134134
let model = try! OTUtils.getAttributeValueFromNative(value)
135135

136-
XCTAssert(model == AttributeValue.others)
137-
138136
let model2 = AttributeValue(value: value)
139-
XCTAssertNil(model2)
137+
XCTAssertEqual(model, model2)
138+
}
139+
140+
func testEncodeDecodeWithDictionaryType() {
141+
let value: [String: Any] = [
142+
"string": "stringvalue",
143+
"double": 13.0,
144+
"bool": true,
145+
"array": ["a", "b", "c"]
146+
]
147+
let model = AttributeValue(value: value)
148+
149+
let encoded = try! OTUtils.getAttributeValueFromNative(value)
150+
print("hello")
151+
XCTAssertEqual(encoded, model)
140152
}
141153

142154
func testDecodeSuccessWithInvalidTypeNil() {
@@ -275,7 +287,7 @@ extension AttributeValueTests {
275287
}
276288

277289
func testEncodeJSON5() {
278-
let modelGiven = [AttributeValue.others]
290+
let modelGiven = [AttributeValue.array([AttributeValue.bool(true), AttributeValue.string("us"), AttributeValue.double(4.7)])]
279291
XCTAssert(OTUtils.isEqualWithEncodeThenDecode(modelGiven))
280292
}
281293

@@ -301,18 +313,17 @@ extension AttributeValueTests {
301313
XCTAssert(model == AttributeValue.bool(valueBool))
302314
XCTAssert(model.description == "bool(\(valueBool))")
303315

304-
let valueOther = [3]
305-
model = try! OTUtils.getAttributeValueFromNative(valueOther)
306-
XCTAssert(model == AttributeValue.others)
307-
XCTAssert(model.description == "others")
308-
316+
let values = [3.0]
317+
model = try! OTUtils.getAttributeValueFromNative(values)
318+
XCTAssert(model == AttributeValue(value: values))
319+
XCTAssert(model.description == "array([double(3.0)])")
309320

310321
let valueInteger = Int64(100)
311322
model = AttributeValue(value: valueInteger)!
312323
XCTAssert(model.description == "int(\(valueInteger))")
313324

314-
let modelOptional = AttributeValue(value: valueOther)
315-
XCTAssertNil(modelOptional)
325+
let modelOptional = AttributeValue(value: values)
326+
XCTAssertNotNil(modelOptional)
316327
}
317328

318329
func testStringValue() {

0 commit comments

Comments
 (0)