DailyView/Carthage/Checkouts/facebook-ios-sdk/FBAEMKit/FBAEMKit/AEMAdvertiserSingleEntryRule.swift
2025-12-30 16:40:31 +08:00

336 lines
11 KiB
Swift

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
import FBSDKCoreKit_Basics
import Foundation
final class AEMAdvertiserSingleEntryRule: NSObject, NSSecureCoding, AEMAdvertiserRuleMatching {
var `operator`: AEMAdvertiserRuleOperator
let paramKey: String
let linguisticCondition: String?
let numericalCondition: Double?
let arrayCondition: [String]?
private enum Keys {
static let `operator` = "operator"
static let param = "param_key"
static let stringValue = "string_value"
static let numberValue = "number_value"
static let arrayValue = "array_value"
}
private enum Delimeter {
static let param = "."
static let asterisk = "[*]"
}
// MRAK: - Init
init(
operator: AEMAdvertiserRuleOperator,
paramKey: String,
linguisticCondition: String?,
numericalCondition: Double?,
arrayCondition: [String]?
) {
self.operator = `operator`
self.paramKey = paramKey
self.linguisticCondition = linguisticCondition
self.numericalCondition = numericalCondition
self.arrayCondition = arrayCondition
super.init()
}
convenience init(
with operator: AEMAdvertiserRuleOperator,
paramKey: String,
linguisticCondition: String?,
numericalCondition: NSNumber?,
arrayCondition: [String]?
) {
self.init(
operator: `operator`,
paramKey: paramKey,
linguisticCondition: linguisticCondition,
numericalCondition: numericalCondition?.doubleValue,
arrayCondition: arrayCondition
)
}
// MARK: - AEMAdvertiserRuleMatching
func isMatchedEventParameters(_ eventParams: [String: Any]?) -> Bool {
let paramPath = paramKey.components(separatedBy: Delimeter.param)
return isMatchedEventParameters(eventParams: eventParams, paramPath: paramPath)
}
func isMatchedEventParameters(eventParams: [String: Any]?, paramPath: [String]) -> Bool {
guard let eventParams = eventParams, !eventParams.isEmpty else {
return false
}
let param = paramPath.first
if let param = param,
param.hasSuffix(Delimeter.asterisk) == true {
return isMatched(withAsteriskParam: param, eventParameters: eventParams, paramPath: paramPath)
}
// if data does not contain the key, we should return false directly.
guard let param = param,
eventParams.keys.contains(param) else {
return false
}
// Apply operator rule if the last param is reached
if paramPath.count == 1 {
var stringValue: String?
var numericalValue: Double?
switch `operator` {
case .contains,
.notContains,
.startsWith,
.caseInsensitiveContains,
.caseInsensitiveNotContains,
.caseInsensitiveStartsWith,
.regexMatch,
.equal,
.notEqual,
.caseInsensitiveIsAny,
.caseInsensitiveIsNotAny,
.isAny,
.isNotAny:
stringValue = eventParams[param] as? String
case .lessThan,
.lessThanOrEqual,
.greaterThan,
.greaterThanOrEqual:
numericalValue = eventParams[param] as? Double
default:
break
}
return isMatched(withStringValue: stringValue, numericalValue: numericalValue)
}
let subParams = eventParams[param] as? [String: Any]
let subParamPath = Array(paramPath.dropFirst())
return isMatchedEventParameters(eventParams: subParams, paramPath: subParamPath)
}
func isMatched(withAsteriskParam param: String, eventParameters: [String: Any], paramPath: [String]) -> Bool {
let length = param.count - Delimeter.asterisk.count
let paramSubstring = String(param[param.startIndex ..< param.index(param.startIndex, offsetBy: length)])
let items = eventParameters[paramSubstring] as? [Any] ?? []
if items.isEmpty || paramPath.count < 2 {
return false
}
var isMatched = false
let subParamPath = Array(paramPath.dropFirst())
for item in items {
isMatched = isMatchedEventParameters(eventParams: item as? [String: Any], paramPath: subParamPath)
if isMatched {
break
}
}
return isMatched
}
// swiftlint:disable:next cyclomatic_complexity
func isMatched(withStringValue stringValue: String?, numericalValue: Double?) -> Bool {
var isMatched = false
switch `operator` {
case .contains:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition,
stringValue.lowercased().contains(linguisticCondition.lowercased()) {
isMatched = true
}
case .notContains:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition {
isMatched = !stringValue.lowercased().contains(linguisticCondition.lowercased())
}
case .startsWith:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition {
isMatched = stringValue.lowercased().hasPrefix(linguisticCondition.lowercased())
}
case .caseInsensitiveContains:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition,
stringValue.lowercased().contains(linguisticCondition.lowercased()) {
isMatched = true
}
case .caseInsensitiveNotContains:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition {
isMatched = !stringValue.lowercased().contains(linguisticCondition.lowercased())
}
case .caseInsensitiveStartsWith:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition,
stringValue.lowercased().hasPrefix(linguisticCondition.lowercased()) {
isMatched = true
}
case .regexMatch:
if let stringValue = stringValue {
isMatched = isRegexMatch(stringValue)
}
case .equal:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition,
stringValue.lowercased() == linguisticCondition.lowercased() {
isMatched = true
}
case .notEqual:
if let stringValue = stringValue,
let linguisticCondition = linguisticCondition {
isMatched = stringValue.lowercased() != linguisticCondition.lowercased()
}
case .caseInsensitiveIsAny:
if let stringValue = stringValue {
isMatched = isAny(of: arrayCondition ?? [], stringValue: stringValue, ignoreCase: true)
}
case .caseInsensitiveIsNotAny:
if let stringValue = stringValue {
return !isAny(of: arrayCondition ?? [], stringValue: stringValue, ignoreCase: true)
}
case .isAny:
if let stringValue = stringValue {
return isAny(of: arrayCondition ?? [], stringValue: stringValue, ignoreCase: false)
}
case .isNotAny:
if let stringValue = stringValue,
!isAny(of: arrayCondition ?? [], stringValue: stringValue, ignoreCase: false) {
isMatched = true
}
case .lessThan:
if let numericalValue = numericalValue,
let numericalCondition = numericalCondition {
isMatched = numericalValue < numericalCondition
}
case .lessThanOrEqual:
if let numericalValue = numericalValue,
let numericalCondition = numericalCondition,
numericalValue <= numericalCondition {
isMatched = true
}
case .greaterThan:
if let numericalValue = numericalValue,
let numericalCondition = numericalCondition,
numericalValue > numericalCondition {
isMatched = true
}
case .greaterThanOrEqual:
if let numericalValue = numericalValue,
let condition = numericalCondition,
numericalValue >= condition {
isMatched = true
}
default:
break
}
return isMatched
}
func isRegexMatch(_ stringValue: String) -> Bool {
guard let linguisticCondition = linguisticCondition, !linguisticCondition.isEmpty else {
return false
}
do {
let regex = try NSRegularExpression(pattern: linguisticCondition, options: .allowCommentsAndWhitespace)
let range = NSRange(location: 0, length: stringValue.count)
let matches = regex.matches(in: stringValue, options: .anchored, range: range)
return !matches.isEmpty
} catch {
return false
}
}
func isAny(of arrayCondition: [String], stringValue: String, ignoreCase: Bool) -> Bool {
var set = Set<String>()
for item in arrayCondition {
if ignoreCase {
set.insert(item.lowercased())
} else {
set.insert(item)
}
}
return set.contains(ignoreCase ? stringValue.lowercased() : stringValue)
}
// MARK: - NSCoding
static var supportsSecureCoding = true
init?(coder: NSCoder) {
let operatorValue = coder.decodeInteger(forKey: Keys.operator)
guard let `operator` = AEMAdvertiserRuleOperator(rawValue: operatorValue),
let paramKey = coder.decodeObject(of: NSString.self, forKey: Keys.param),
let linguisticCondition = coder.decodeObject(of: NSString.self, forKey: Keys.stringValue),
let numericalCondition = coder.decodeObject(of: NSNumber.self, forKey: Keys.numberValue) else {
return nil
}
let arrayCondition = coder.decodeObject(of: [NSArray.self, NSString.self], forKey: Keys.arrayValue) as? [String]
self.operator = `operator`
self.paramKey = paramKey as String
self.linguisticCondition = linguisticCondition as String
self.numericalCondition = numericalCondition.doubleValue
self.arrayCondition = arrayCondition
super.init()
}
func encode(with coder: NSCoder) {
coder.encode(`operator`.rawValue, forKey: Keys.operator)
coder.encode(paramKey, forKey: Keys.param)
coder.encode(linguisticCondition, forKey: Keys.stringValue)
coder.encode(numericalCondition, forKey: Keys.numberValue)
coder.encode(arrayCondition, forKey: Keys.arrayValue)
}
override func isEqual(_ object: Any?) -> Bool {
if let rule = object as? AEMAdvertiserSingleEntryRule {
let isOpEqual = self.operator == rule.operator
let isParamKeyEqual = paramKey == rule.paramKey
let isLinguisticConditionEqual = linguisticCondition == rule.linguisticCondition
var isArrayConditionEqual = false
if let array1 = arrayCondition {
let array2 = rule.arrayCondition
isArrayConditionEqual = array1 == array2
} else {
isArrayConditionEqual = rule.arrayCondition == nil
}
let isNumericConditionEqual = ((numericalCondition == nil && rule.numericalCondition == nil)
|| (numericalCondition == rule.numericalCondition) == true)
return isOpEqual && isParamKeyEqual && isLinguisticConditionEqual
&& isArrayConditionEqual && isNumericConditionEqual
}
return false
}
}