Skip to content

Commit

Permalink
Merge 622832f into 9d39558
Browse files Browse the repository at this point in the history
  • Loading branch information
jaeopt committed Aug 10, 2022
2 parents 9d39558 + 622832f commit 6180f16
Show file tree
Hide file tree
Showing 69 changed files with 5,693 additions and 561 deletions.
2 changes: 2 additions & 0 deletions DemoSwiftApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
@unknown default:
print("Optimizely SDK initiliazation failed with unknown result")
}

self.startWithRootViewController()

// For sample codes for APIs, see "Samples/SamplesForAPI.swift"
//SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely)
//SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely)
//SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely)
}
}

Expand Down
2 changes: 1 addition & 1 deletion DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1230;
LastUpgradeCheck = 1250;
LastUpgradeCheck = 1320;
ORGANIZATIONNAME = Optimizely;
TargetAttributes = {
252D7DEC21C8800800134A7A = {
Expand Down
36 changes: 31 additions & 5 deletions DemoSwiftApp/Samples/SamplesForAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import Foundation
import Optimizely
import UIKit

class SamplesForAPI {

Expand Down Expand Up @@ -146,17 +147,17 @@ class SamplesForAPI {

// (1) set a forced decision for a flag

var success = user.setForcedDecision(context: context1, decision: forced1)
_ = user.setForcedDecision(context: context1, decision: forced1)
decision = user.decide(key: "flag-1")

// (2) set a forced decision for an ab-test rule

success = user.setForcedDecision(context: context2, decision: forced2)
_ = user.setForcedDecision(context: context2, decision: forced2)
decision = user.decide(key: "flag-1")

// (3) set a forced variation for a delivery rule

success = user.setForcedDecision(context: context3,
_ = user.setForcedDecision(context: context3,
decision: forced3)
decision = user.decide(key: "flag-1")

Expand All @@ -167,8 +168,8 @@ class SamplesForAPI {

// (5) remove forced variations

success = user.removeForcedDecision(context: context2)
success = user.removeAllForcedDecisions()
_ = user.removeForcedDecision(context: context2)
_ = user.removeAllForcedDecisions()
}

// MARK: - OptimizelyConfig
Expand Down Expand Up @@ -260,6 +261,31 @@ class SamplesForAPI {

}

// MARK: - AudienceSegments

static func checkAudienceSegments(optimizely: OptimizelyClient) {
// override the default handler if cache size and timeout need to be customized
let optimizely = OptimizelyClient(sdkKey: "FCnSegiEkRry9rhVMroit4",
periodicDownloadInterval: 60)
optimizely.start { result in
if case .failure(let error) = result {
print("[AudienceSegments] SDK initialization failed: \(error)")
return
}

let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"])
user.fetchQualifiedSegments(options: [.ignoreCache]) { _, error in
guard error == nil else {
print("[AudienceSegments] \(error!.errorDescription!)")
return
}

let decision = user.decide(key: "show_coupon", options: [.includeReasons])
print("[AudienceSegments] decision: \(decision)")
}
}
}

// MARK: - Initializations

static func samplesForInitialization() {
Expand Down
614 changes: 579 additions & 35 deletions OptimizelySwiftSDK.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<dict>
<key>FILEHEADER</key>
<string>
// Copyright 2021, Optimizely, Inc. and contributors
// 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.
Expand Down
32 changes: 30 additions & 2 deletions Sources/Data Model/Audience/Audience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,36 @@ struct Audience: Codable, Equatable, OptimizelyAudience {
try container.encode(conditionHolder, forKey: .conditions)
}

func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
return try conditionHolder.evaluate(project: project, attributes: attributes)
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
return try conditionHolder.evaluate(project: project, user: user)
}

/// Extract all audience segments used in this audience conditions.
/// - Returns: a String array of segment names.
func getSegments() -> [String] {
let segments = getSegments(condition: conditionHolder)
return Array(Set(segments))
}

func getSegments(condition: ConditionHolder) -> [String] {
var segments = [String]()

switch condition {
case .logicalOp:
return []
case .leaf(let leaf):
if case .attribute(let userAttribute) = leaf {
if userAttribute.matchSupported == .qualified, let strValue = userAttribute.value?.stringValue {
segments.append(strValue)
}
}
case .array(let conditions):
conditions.forEach {
segments.append(contentsOf: getSegments(condition: $0))
}
}

return segments
}

}
16 changes: 8 additions & 8 deletions Sources/Data Model/Audience/ConditionHolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ enum ConditionHolder: Codable, Equatable {
}
}

func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
switch self {
case .logicalOp:
throw OptimizelyError.conditionInvalidFormat("Logical operation not evaluated")
case .leaf(let conditionLeaf):
return try conditionLeaf.evaluate(project: project, attributes: attributes)
return try conditionLeaf.evaluate(project: project, user: user)
case .array(let conditions):
return try conditions.evaluate(project: project, attributes: attributes)
return try conditions.evaluate(project: project, user: user)
}
}

Expand Down Expand Up @@ -111,24 +111,24 @@ extension ConditionHolder {

extension Array where Element == ConditionHolder {

func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
guard let firstItem = self.first else {
throw OptimizelyError.conditionInvalidFormat("Empty condition array")
}

switch firstItem {
case .logicalOp(let op):
return try evaluate(op: op, project: project, attributes: attributes)
return try evaluate(op: op, project: project, user: user)
case .leaf:
// special case - no logical operator
// implicit or
return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, attributes: attributes)
return try [[ConditionHolder.logicalOp(.or)], self].flatMap({$0}).evaluate(op: LogicalOp.or, project: project, user: user)
default:
throw OptimizelyError.conditionInvalidFormat("Invalid first item")
}
}

func evaluate(op: LogicalOp, project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
func evaluate(op: LogicalOp, project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
guard self.count > 0 else {
throw OptimizelyError.conditionInvalidFormat("Empty condition array")
}
Expand All @@ -138,7 +138,7 @@ extension Array where Element == ConditionHolder {
// create closure array for delayed evaluations to avoid unnecessary ops
let evalList = itemsAfterOpTrimmed.map { holder -> ThrowableCondition in
return {
return try holder.evaluate(project: project, attributes: attributes)
return try holder.evaluate(project: project, user: user)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/Data Model/Audience/ConditionLeaf.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ enum ConditionLeaf: Codable, Equatable {
}
}

func evaluate(project: ProjectProtocol?, attributes: OptimizelyAttributes?) throws -> Bool {
func evaluate(project: ProjectProtocol?, user: OptimizelyUserContext) throws -> Bool {
switch self {
case .audienceId(let id):
guard let project = project else {
throw OptimizelyError.conditionCannotBeEvaluated("audienceId: \(id)")
}

return try project.evaluateAudience(audienceId: id, attributes: attributes)
return try project.evaluateAudience(audienceId: id, user: user)
case .attribute(let userAttribute):
return try userAttribute.evaluate(attributes: attributes)
return try userAttribute.evaluate(user: user)
}
}

Expand Down
68 changes: 42 additions & 26 deletions Sources/Data Model/Audience/UserAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct UserAttribute: Codable, Equatable {

enum ConditionType: String, Codable {
case customAttribute = "custom_attribute"
case thirdPartyDimension = "third_party_dimension"
}

enum ConditionMatch: String, Codable {
Expand All @@ -52,6 +53,7 @@ struct UserAttribute: Codable, Equatable {
case semver_le
case semver_gt
case semver_ge
case qualified
}

var typeSupported: ConditionType? {
Expand Down Expand Up @@ -98,7 +100,7 @@ struct UserAttribute: Codable, Equatable {

extension UserAttribute {

func evaluate(attributes: OptimizelyAttributes?) throws -> Bool {
func evaluate(user: OptimizelyUserContext) throws -> Bool {

// invalid type - parsed for forward compatibility only (but evaluation fails)
if typeSupported == nil {
Expand All @@ -114,63 +116,77 @@ extension UserAttribute {
throw OptimizelyError.userAttributeInvalidName(stringRepresentation)
}

let attributes = attributes ?? OptimizelyAttributes()

let rawAttributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
let attributes = user.attributes
let rawValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"

if matchFinal != .exists {
if !attributes.keys.contains(nameFinal) {
throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal)
}
if matchFinal == .exists {
return !(rawValue is NSNull || rawValue == nil)
}

// all other matches requires valid value

if value == nil {
throw OptimizelyError.userAttributeNilValue(stringRepresentation)
}
guard let value = value else {
throw OptimizelyError.userAttributeNilValue(stringRepresentation)
}

if rawAttributeValue == nil {
throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal)
if matchFinal == .qualified {
// NOTE: name ("odp.audiences") and type("third_party_dimension") not used

guard case .string(let strValue) = value else {
throw OptimizelyError.evaluateAttributeInvalidCondition(stringRepresentation)
}
return user.isQualifiedFor(segment: strValue)
}

// all other matches requires attribute value

guard attributes.keys.contains(nameFinal) else {
throw OptimizelyError.missingAttributeValue(stringRepresentation, nameFinal)
}

guard let rawAttributeValue = rawValue else {
throw OptimizelyError.nilAttributeValue(stringRepresentation, nameFinal)
}

switch matchFinal {
case .exists:
return !(rawAttributeValue is NSNull || rawAttributeValue == nil)
case .exact:
return try value!.isExactMatch(with: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isExactMatch(with: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
case .substring:
return try value!.isSubstring(of: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isSubstring(of: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
case .lt:
// user attribute "less than" this condition value
// so evaluate if this condition value "isGreater" than the user attribute value
return try value!.isGreater(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isGreater(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
case .le:
// user attribute "less than" or equal this condition value
// so evaluate if this condition value "isGreater" than or equal the user attribute value
return try value!.isGreaterOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isGreaterOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
case .gt:
// user attribute "greater than" this condition value
// so evaluate if this condition value "isLess" than the user attribute value
return try value!.isLess(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isLess(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
case .ge:
// user attribute "greater than or equal" this condition value
// so evaluate if this condition value "isLess" than or equal the user attribute value
return try value!.isLessOrEqual(than: rawAttributeValue!, condition: stringRepresentation, name: nameFinal)
return try value.isLessOrEqual(than: rawAttributeValue, condition: stringRepresentation, name: nameFinal)
// semantic versioning seems unique. the comarison is to compare verion but the passed in version is the target version.
case .semver_eq:
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
return try targetValue.isSemanticVersionEqual(than: value!.stringValue)
return try targetValue.isSemanticVersionEqual(than: value.stringValue)
case .semver_lt:
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
return try targetValue.isSemanticVersionLess(than: value!.stringValue)
return try targetValue.isSemanticVersionLess(than: value.stringValue)
case .semver_le:
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
return try targetValue.isSemanticVersionLessOrEqual(than: value!.stringValue)
return try targetValue.isSemanticVersionLessOrEqual(than: value.stringValue)
case .semver_gt:
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
return try targetValue.isSemanticVersionGreater(than: value!.stringValue)
return try targetValue.isSemanticVersionGreater(than: value.stringValue)
case .semver_ge:
let targetValue = try targetAsAttributeValue(value: rawAttributeValue, attribute: value, nameFinal: nameFinal)
return try targetValue.isSemanticVersionGreaterOrEqual(than: value!.stringValue)
return try targetValue.isSemanticVersionGreaterOrEqual(than: value.stringValue)
default:
throw OptimizelyError.userAttributeInvalidMatch(stringRepresentation)
}
}

Expand Down
23 changes: 23 additions & 0 deletions Sources/Data Model/Integration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// 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 Integration: Codable, Equatable {
var key: String
var host: String?
var publicKey: String?
}
Loading

0 comments on commit 6180f16

Please sign in to comment.