Skip to content

Commit 2b68de8

Browse files
Merge branch 'master' into prepare-release-5.1.1
* master: Experiment id and variation id added into decision notification payload (#589) [FSSDK-11374] add test cases for impression event and decision listener with holdout support (#588) [FSSDK-11373] add holdout support and refactor decision logic in DefaultDecisionService (#587) [FSSDK-11372] assign holdoutIds to feature flags (#578) [FSSDK-11371] parsing holdout section from the datafile (#577) [FSSDK-11111] chore: update cocoapods version (#576) [FSSDK-11102] chore: replace travis-ci-tools repository (#571)
2 parents be6e381 + 4dffe6d commit 2b68de8

34 files changed

+3496
-191
lines changed

.github/workflows/integration_tests.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ on:
55
secrets:
66
CI_USER_TOKEN:
77
required: true
8-
TRAVIS_COM_TOKEN:
9-
required: true
10-
118
jobs:
129
integration_tests:
1310
runs-on: ubuntu-latest
@@ -16,8 +13,8 @@ jobs:
1613
with:
1714
# You should create a personal access token and store it in your repository
1815
token: ${{ secrets.CI_USER_TOKEN }}
19-
repository: 'optimizely/travisci-tools'
20-
path: 'home/runner/travisci-tools'
16+
repository: 'optimizely/ci-helper-tools'
17+
path: 'home/runner/ci-helper-tools'
2118
ref: 'master'
2219
- name: set SDK Branch if PR
2320
env:
@@ -31,7 +28,6 @@ jobs:
3128
if: ${{ github.event_name != 'pull_request' }}
3229
run: |
3330
echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV
34-
echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV
3531
- name: Trigger build
3632
env:
3733
SDK: swift
@@ -41,14 +37,12 @@ jobs:
4137
GITHUB_TOKEN: ${{ secrets.CI_USER_TOKEN }}
4238
EVENT_TYPE: ${{ github.event_name }}
4339
GITHUB_CONTEXT: ${{ toJson(github) }}
44-
#REPO_SLUG: ${{ github.repository }}
4540
PULL_REQUEST_SLUG: ${{ github.repository }}
4641
UPSTREAM_REPO: ${{ github.repository }}
4742
PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }}
4843
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
4944
UPSTREAM_SHA: ${{ github.sha }}
50-
TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
5145
EVENT_MESSAGE: ${{ github.event.message }}
5246
HOME: 'home/runner'
5347
run: |
54-
home/runner/travisci-tools/trigger-script-with-status-update.sh
48+
home/runner/ci-helper-tools/trigger-script-with-status-update.sh

.github/workflows/swift.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ jobs:
2929
uses: optimizely/swift-sdk/.github/workflows/integration_tests.yml@master
3030
secrets:
3131
CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }}
32-
TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }}
3332

3433
lint:
3534
runs-on: macos-13
@@ -41,14 +40,13 @@ jobs:
4140
- env:
4241
SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }}
4342
run: |
44-
gem install cocoapods -v '1.9.3'
43+
gem install cocoapods -v '1.15.2'
4544
pod spec lint --quick
4645
curl -sSL https://download.sourceclear.com/ci.sh | bash
4746
4847
unittests:
4948
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
5049
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master
51-
5250
prepare_for_release:
5351
runs-on: macos-13
5452
if: "${{ github.event.inputs.PREP == 'true' && github.event_name == 'workflow_dispatch' }}"
@@ -69,7 +67,7 @@ jobs:
6967
BRANCH: ${{ github.ref_name }}
7068
GITHUB_USER: optibot
7169
GITHUB_TOKEN: ${{ secrets.CI_USER_TOKEN }}
72-
COCOAPODS_VERSION: '1.12.1'
70+
COCOAPODS_VERSION: '1.15.2'
7371
run: |
7472
gem install cocoapods -v $COCOAPODS_VERSION
7573
Scripts/run_prep.sh

.github/workflows/unit_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
NAME: ${{ matrix.device }}
7777
run: |
7878
gem install coveralls-lcov
79-
gem install cocoapods -v '1.11.3'
79+
gem install cocoapods -v '1.15.2'
8080
pod repo update
8181
pod install
8282
HOMEBREW_NO_INSTALL_CLEANUP=true brew update && brew install jq

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 171 additions & 1 deletion
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@ Used to enforce Swift style and conventions.
129129

130130
- React - https://github.com/optimizely/react-sdk
131131

132-
- Ruby - https://github.com/optimizely/ruby-sdk
132+
- Ruby - https://github.com/optimizely/ruby-sdk

Sources/Data Model/Experiment.swift

Lines changed: 1 addition & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import Foundation
1818

19-
struct Experiment: Codable, OptimizelyExperiment {
19+
struct Experiment: Codable, ExperimentCore {
2020
enum Status: String, Codable {
2121
case running = "Running"
2222
case launched = "Launched"
@@ -64,74 +64,9 @@ extension Experiment: Equatable {
6464
// MARK: - Utils
6565

6666
extension Experiment {
67-
func getVariation(id: String) -> Variation? {
68-
return variations.filter { $0.id == id }.first
69-
}
70-
71-
func getVariation(key: String) -> Variation? {
72-
return variations.filter { $0.key == key }.first
73-
}
7467

7568
var isActivated: Bool {
7669
return status == .running
7770
}
7871

79-
mutating func serializeAudiences(with audiencesMap: [String: String]) {
80-
guard let conditions = audienceConditions else { return }
81-
82-
let serialized = conditions.serialized
83-
audiences = replaceAudienceIdsWithNames(string: serialized, audiencesMap: audiencesMap)
84-
}
85-
86-
/// Replace audience ids with audience names
87-
///
88-
/// example:
89-
/// - string: "(AUDIENCE(1) OR AUDIENCE(2)) AND AUDIENCE(3)"
90-
/// - replaced: "(\"us\" OR \"female\") AND \"adult\""
91-
///
92-
/// - Parameter string: before replacement
93-
/// - Returns: string after replacement
94-
func replaceAudienceIdsWithNames(string: String, audiencesMap: [String: String]) -> String {
95-
let beginWord = "AUDIENCE("
96-
let endWord = ")"
97-
var keyIdx = 0
98-
var audienceId = ""
99-
var collect = false
100-
101-
var replaced = ""
102-
for ch in string {
103-
// extract audience id in parenthesis (example: AUDIENCE("35") => "35")
104-
if collect {
105-
if String(ch) == endWord {
106-
// output the extracted audienceId
107-
replaced += "\"\(audiencesMap[audienceId] ?? audienceId)\""
108-
collect = false
109-
audienceId = ""
110-
} else {
111-
audienceId += String(ch)
112-
}
113-
continue
114-
}
115-
116-
// walk-through until finding a matching keyword "AUDIENCE("
117-
if ch == Array(beginWord)[keyIdx] {
118-
keyIdx += 1
119-
if keyIdx == beginWord.count {
120-
keyIdx = 0
121-
collect = true
122-
}
123-
continue
124-
} else {
125-
if keyIdx > 0 {
126-
replaced += Array(beginWord)[..<keyIdx]
127-
}
128-
keyIdx = 0
129-
}
130-
131-
// pass through other characters
132-
replaced += String(ch)
133-
}
134-
135-
return replaced
136-
}
13772
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// Copyright 2022, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
19+
protocol ExperimentCore: OptimizelyExperiment {
20+
var audiences: String { get set }
21+
var layerId: String { get }
22+
var variations: [Variation] { get }
23+
var trafficAllocation: [TrafficAllocation] { get }
24+
var audienceIds: [String] { get }
25+
var audienceConditions: ConditionHolder? { get }
26+
}
27+
28+
// Shared utilities in an extension
29+
extension ExperimentCore {
30+
func getVariation(id: String) -> Variation? {
31+
return variations.filter { $0.id == id }.first
32+
}
33+
34+
func getVariation(key: String) -> Variation? {
35+
return variations.filter { $0.key == key }.first
36+
}
37+
38+
func replaceAudienceIdsWithNames(string: String, audiencesMap: [String: String]) -> String {
39+
let beginWord = "AUDIENCE("
40+
let endWord = ")"
41+
var keyIdx = 0
42+
var audienceId = ""
43+
var collect = false
44+
45+
var replaced = ""
46+
for ch in string {
47+
if collect {
48+
if String(ch) == endWord {
49+
replaced += "\"\(audiencesMap[audienceId] ?? audienceId)\""
50+
collect = false
51+
audienceId = ""
52+
} else {
53+
audienceId += String(ch)
54+
}
55+
continue
56+
}
57+
58+
if ch == Array(beginWord)[keyIdx] {
59+
keyIdx += 1
60+
if keyIdx == beginWord.count {
61+
keyIdx = 0
62+
collect = true
63+
}
64+
continue
65+
} else {
66+
if keyIdx > 0 {
67+
replaced += Array(beginWord)[..<keyIdx]
68+
}
69+
keyIdx = 0
70+
}
71+
72+
replaced += String(ch)
73+
}
74+
75+
return replaced
76+
}
77+
78+
mutating func serializeAudiences(with audiencesMap: [String: String]) {
79+
guard let conditions = audienceConditions else { return }
80+
81+
let serialized = conditions.serialized
82+
audiences = replaceAudienceIdsWithNames(string: serialized, audiencesMap: audiencesMap)
83+
}
84+
}

Sources/Data Model/Holdout.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// Copyright 2022, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
19+
struct Holdout: Codable, ExperimentCore {
20+
enum Status: String, Codable {
21+
case draft = "Draft"
22+
case running = "Running"
23+
case concluded = "Concluded"
24+
case archived = "Archived"
25+
}
26+
27+
var id: String
28+
var key: String
29+
var status: Status
30+
var layerId: String
31+
var variations: [Variation]
32+
var trafficAllocation: [TrafficAllocation]
33+
var audienceIds: [String]
34+
var audienceConditions: ConditionHolder?
35+
var includedFlags: [String]
36+
var excludedFlags: [String]
37+
38+
enum CodingKeys: String, CodingKey {
39+
case id, key, status, layerId, variations, trafficAllocation, audienceIds, audienceConditions, includedFlags, excludedFlags
40+
}
41+
42+
var variationsMap: [String: OptimizelyVariation] = [:]
43+
// replace with serialized string representation with audience names when ProjectConfig is ready
44+
var audiences: String = ""
45+
46+
init(from decoder: Decoder) throws {
47+
let container = try decoder.container(keyedBy: CodingKeys.self)
48+
49+
id = try container.decode(String.self, forKey: .id)
50+
key = try container.decode(String.self, forKey: .key)
51+
status = try container.decode(Status.self, forKey: .status)
52+
layerId = try container.decode(String.self, forKey: .layerId)
53+
variations = try container.decode([Variation].self, forKey: .variations)
54+
trafficAllocation = try container.decode([TrafficAllocation].self, forKey: .trafficAllocation)
55+
audienceIds = try container.decode([String].self, forKey: .audienceIds)
56+
audienceConditions = try container.decodeIfPresent(ConditionHolder.self, forKey: .audienceConditions)
57+
58+
includedFlags = try container.decodeIfPresent([String].self, forKey: .includedFlags) ?? []
59+
excludedFlags = try container.decodeIfPresent([String].self, forKey: .excludedFlags) ?? []
60+
}
61+
}
62+
63+
extension Holdout: Equatable {
64+
static func == (lhs: Holdout, rhs: Holdout) -> Bool {
65+
return lhs.id == rhs.id &&
66+
lhs.key == rhs.key &&
67+
lhs.status == rhs.status &&
68+
lhs.layerId == rhs.layerId &&
69+
lhs.variations == rhs.variations &&
70+
lhs.trafficAllocation == rhs.trafficAllocation &&
71+
lhs.audienceIds == rhs.audienceIds &&
72+
lhs.audienceConditions == rhs.audienceConditions &&
73+
lhs.includedFlags == rhs.includedFlags &&
74+
lhs.excludedFlags == rhs.excludedFlags
75+
}
76+
}
77+
78+
extension Holdout {
79+
var isActivated: Bool {
80+
return status == .running
81+
}
82+
}

0 commit comments

Comments
 (0)