Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion OptimizelySwiftSDK.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

67 changes: 1 addition & 66 deletions Sources/Data Model/Experiment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import Foundation

struct Experiment: Codable, OptimizelyExperiment {
struct Experiment: Codable, ExperimentCore {
enum Status: String, Codable {
case running = "Running"
case launched = "Launched"
Expand Down Expand Up @@ -64,74 +64,9 @@ extension Experiment: Equatable {
// MARK: - Utils

extension Experiment {
func getVariation(id: String) -> Variation? {
return variations.filter { $0.id == id }.first
}

func getVariation(key: String) -> Variation? {
return variations.filter { $0.key == key }.first
}

var isActivated: Bool {
return status == .running
}

mutating func serializeAudiences(with audiencesMap: [String: String]) {
guard let conditions = audienceConditions else { return }

let serialized = conditions.serialized
audiences = replaceAudienceIdsWithNames(string: serialized, audiencesMap: audiencesMap)
}

/// Replace audience ids with audience names
///
/// example:
/// - string: "(AUDIENCE(1) OR AUDIENCE(2)) AND AUDIENCE(3)"
/// - replaced: "(\"us\" OR \"female\") AND \"adult\""
///
/// - Parameter string: before replacement
/// - Returns: string after replacement
func replaceAudienceIdsWithNames(string: String, audiencesMap: [String: String]) -> String {
let beginWord = "AUDIENCE("
let endWord = ")"
var keyIdx = 0
var audienceId = ""
var collect = false

var replaced = ""
for ch in string {
// extract audience id in parenthesis (example: AUDIENCE("35") => "35")
if collect {
if String(ch) == endWord {
// output the extracted audienceId
replaced += "\"\(audiencesMap[audienceId] ?? audienceId)\""
collect = false
audienceId = ""
} else {
audienceId += String(ch)
}
continue
}

// walk-through until finding a matching keyword "AUDIENCE("
if ch == Array(beginWord)[keyIdx] {
keyIdx += 1
if keyIdx == beginWord.count {
keyIdx = 0
collect = true
}
continue
} else {
if keyIdx > 0 {
replaced += Array(beginWord)[..<keyIdx]
}
keyIdx = 0
}

// pass through other characters
replaced += String(ch)
}

return replaced
}
}
86 changes: 86 additions & 0 deletions Sources/Data Model/ExperimentCore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// Copyright 2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

protocol ExperimentCore {
var id: String { get }
var key: String { get }
var audiences: String { get set }
var layerId: String { get }
var variations: [Variation] { get }
var trafficAllocation: [TrafficAllocation] { get }
var audienceIds: [String] { get }
var audienceConditions: ConditionHolder? { get }
}

// Shared utilities in an extension
extension ExperimentCore {
func getVariation(id: String) -> Variation? {
return variations.filter { $0.id == id }.first
}

func getVariation(key: String) -> Variation? {
return variations.filter { $0.key == key }.first
}

func replaceAudienceIdsWithNames(string: String, audiencesMap: [String: String]) -> String {
let beginWord = "AUDIENCE("
let endWord = ")"
var keyIdx = 0
var audienceId = ""
var collect = false

var replaced = ""
for ch in string {
if collect {
if String(ch) == endWord {
replaced += "\"\(audiencesMap[audienceId] ?? audienceId)\""
collect = false
audienceId = ""
} else {
audienceId += String(ch)
}
continue
}

if ch == Array(beginWord)[keyIdx] {
keyIdx += 1
if keyIdx == beginWord.count {
keyIdx = 0
collect = true
}
continue
} else {
if keyIdx > 0 {
replaced += Array(beginWord)[..<keyIdx]
}
keyIdx = 0
}

replaced += String(ch)
}

return replaced
}

mutating func serializeAudiences(with audiencesMap: [String: String]) {
guard let conditions = audienceConditions else { return }

let serialized = conditions.serialized
audiences = replaceAudienceIdsWithNames(string: serialized, audiencesMap: audiencesMap)
}
}
83 changes: 83 additions & 0 deletions Sources/Data Model/Holdout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright 2022, Optimizely, Inc. and contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

struct Holdout: Codable, ExperimentCore {
enum Status: String, Codable {
case draft = "Draft"
case running = "Running"
case concluded = "Concluded"
case archived = "Archived"
}

var id: String
var key: String
var status: Status
var layerId: String
var variations: [Variation]
var trafficAllocation: [TrafficAllocation]
var audienceIds: [String]
var audienceConditions: ConditionHolder?
var includedFlags: [String]
var excludedFlags: [String]

enum CodingKeys: String, CodingKey {
case id, key, status, layerId, variations, trafficAllocation, audienceIds, audienceConditions, includedFlags, excludedFlags
}

// replace with serialized string representation with audience names when ProjectConfig is ready
var audiences: String = ""


init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

id = try container.decode(String.self, forKey: .id)
key = try container.decode(String.self, forKey: .key)
status = try container.decode(Status.self, forKey: .status)
layerId = try container.decode(String.self, forKey: .layerId)
variations = try container.decode([Variation].self, forKey: .variations)
trafficAllocation = try container.decode([TrafficAllocation].self, forKey: .trafficAllocation)
audienceIds = try container.decode([String].self, forKey: .audienceIds)
audienceConditions = try container.decodeIfPresent(ConditionHolder.self, forKey: .audienceConditions)

includedFlags = try container.decodeIfPresent([String].self, forKey: .includedFlags) ?? []
excludedFlags = try container.decodeIfPresent([String].self, forKey: .excludedFlags) ?? []
}
}

extension Holdout: Equatable {
static func == (lhs: Holdout, rhs: Holdout) -> Bool {
return lhs.id == rhs.id &&
lhs.key == rhs.key &&
lhs.status == rhs.status &&
lhs.layerId == rhs.layerId &&
lhs.variations == rhs.variations &&
lhs.trafficAllocation == rhs.trafficAllocation &&
lhs.audienceIds == rhs.audienceIds &&
lhs.audienceConditions == rhs.audienceConditions &&
lhs.includedFlags == rhs.includedFlags &&
lhs.excludedFlags == rhs.excludedFlags
}
}


extension Holdout {
var isActivated: Bool {
return status == .running
}
}
3 changes: 3 additions & 0 deletions Sources/Optimizely/OptimizelyConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public protocol OptimizelyExperiment {
var variationsMap: [String: OptimizelyVariation] { get }
}

// Experiment compliances OptimizelyExperiment
extension Experiment: OptimizelyExperiment { }

public protocol OptimizelyFeature {
var id: String { get }
var key: String { get }
Expand Down
Loading
Loading