diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 1bd63f5..4c009be 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -8,49 +8,66 @@ import Foundation /** -A Flag represents a feature flag on the server. -*/ + A Flag represents a feature flag on the server. + */ public struct Flag: Codable, Sendable { - enum CodingKeys: String, CodingKey { - case feature - case value = "feature_state_value" - case enabled - } - - public let feature: Feature - public let value: TypedValue - public let enabled: Bool - - public init(featureName:String, boolValue: Bool, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.init(featureName: featureName, value: TypedValue.bool(boolValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) - } - - public init(featureName:String, floatValue: Float, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.init(featureName: featureName, value: TypedValue.float(floatValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) - } - - public init(featureName:String, intValue: Int, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.init(featureName: featureName, value: TypedValue.int(intValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) - } - - public init(featureName:String, stringValue: String, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.init(featureName: featureName, value: TypedValue.string(stringValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) - } - - public init(featureName:String, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.init(featureName: featureName, value: TypedValue.null, enabled: enabled, featureType: featureType, featureDescription: featureDescription) - } - - public init(featureName:String, value: TypedValue, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { - self.feature = Feature(name: featureName, type: featureType, description: featureDescription) - self.value = value - self.enabled = enabled - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.feature, forKey: .feature) - try container.encode(self.value, forKey: .value) - try container.encode(self.enabled, forKey: .enabled) - } + enum CodingKeys: String, CodingKey { + case feature + case value = "feature_state_value" + case enabled + } + + public let feature: Feature + public let value: TypedValue + public let enabled: Bool + + public init(featureName: String, boolValue: Bool, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + self.init(featureName: featureName, value: TypedValue.bool(boolValue), enabled: enabled, + featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName: String, floatValue: Float, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + self.init(featureName: featureName, value: TypedValue.float(floatValue), enabled: enabled, + featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName: String, intValue: Int, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + self.init(featureName: featureName, value: TypedValue.int(intValue), enabled: enabled, + featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName: String, stringValue: String, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + self.init(featureName: featureName, value: TypedValue.string(stringValue), enabled: enabled, + featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName: String, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + self.init(featureName: featureName, value: TypedValue.null, enabled: enabled, + featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName: String, value: TypedValue, enabled: Bool, + featureType: String? = nil, featureDescription: String? = nil) + { + feature = Feature(name: featureName, type: featureType, description: featureDescription) + self.value = value + self.enabled = enabled + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(feature, forKey: .feature) + try container.encode(value, forKey: .value) + try container.encode(enabled, forKey: .enabled) + } } diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 777d489..7a1a967 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -7,14 +7,14 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif /// Manage feature flags and remote config across multiple projects, /// environments and organisations. public final class Flagsmith: @unchecked Sendable { /// Shared singleton client object - public static let shared: Flagsmith = Flagsmith() + public static let shared: Flagsmith = .init() private let apiManager: APIManager private let analytics: FlagsmithAnalytics @@ -22,33 +22,33 @@ public final class Flagsmith: @unchecked Sendable { /// /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. public var baseURL: URL { - set { apiManager.baseURL = newValue } get { apiManager.baseURL } + set { apiManager.baseURL = newValue } } /// API Key unique to your organization. /// /// This value must be provided before any request can succeed. public var apiKey: String? { - set { apiManager.apiKey = newValue } get { apiManager.apiKey } + set { apiManager.apiKey = newValue } } /// Is flag analytics enabled? public var enableAnalytics: Bool { - set { analytics.enableAnalytics = newValue } get { analytics.enableAnalytics } + set { analytics.enableAnalytics = newValue } } /// How often to send the flag analytics, in seconds public var analyticsFlushPeriod: Int { - set { analytics.flushPeriod = newValue } get { analytics.flushPeriod } + set { analytics.flushPeriod = newValue } } /// Default flags to fall back on if an API call fails private var _defaultFlags: [Flag] = [] - public var defaultFlags: [Flag] { + public var defaultFlags: [Flag] { get { apiManager.propertiesSerialAccessQueue.sync { _defaultFlags } } @@ -60,8 +60,8 @@ public final class Flagsmith: @unchecked Sendable { } /// Configuration class for the cache settings - private var _cacheConfig: CacheConfig = CacheConfig() - public var cacheConfig: CacheConfig { + private var _cacheConfig: CacheConfig = .init() + public var cacheConfig: CacheConfig { get { apiManager.propertiesSerialAccessQueue.sync { _cacheConfig } } @@ -83,18 +83,17 @@ public final class Flagsmith: @unchecked Sendable { /// - identity: ID of the user (optional) /// - completion: Closure with Result which contains array of Flag objects in case of success or Error in case of failure public func getFeatureFlags(forIdentity identity: String? = nil, - completion: @Sendable @escaping (Result<[Flag], any Error>) -> Void) { - + completion: @Sendable @escaping (Result<[Flag], any Error>) -> Void) + { if let identity = identity { - getIdentity(identity) { (result) in + getIdentity(identity) { result in switch result { - case .success(let thisIdentity): + case let .success(thisIdentity): completion(.success(thisIdentity.flags)) - case .failure(let error): + case let .failure(error): if self.defaultFlags.isEmpty { completion(.failure(error)) - } - else { + } else { completion(.success(self.defaultFlags)) } } @@ -102,13 +101,12 @@ public final class Flagsmith: @unchecked Sendable { } else { apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in switch result { - case .success(let flags): + case let .success(flags): completion(.success(flags)) - case .failure(let error): + case let .failure(error): if self.defaultFlags.isEmpty { completion(.failure(error)) - } - else { + } else { completion(.success(self.defaultFlags)) } } @@ -124,18 +122,18 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result which contains Bool in case of success or Error in case of failure public func hasFeatureFlag(withID id: String, forIdentity identity: String? = nil, - completion: @Sendable @escaping (Result) -> Void) { + completion: @Sendable @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in + getFeatureFlags(forIdentity: identity) { result in switch result { - case .success(let flags): - let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) + case let .success(flags): + let hasFlag = flags.contains(where: { $0.feature.name == id && $0.enabled }) completion(.success(hasFlag)) - case .failure(let error): - if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { + case let .failure(error): + if self.defaultFlags.contains(where: { $0.feature.name == id && $0.enabled }) { completion(.success(true)) - } - else { + } else { completion(.failure(error)) } } @@ -151,18 +149,18 @@ public final class Flagsmith: @unchecked Sendable { @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") public func getFeatureValue(withID id: String, forIdentity identity: String? = nil, - completion: @Sendable @escaping (Result) -> Void) { + completion: @Sendable @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in + getFeatureFlags(forIdentity: identity) { result in switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) + case let .success(flags): + let flag = flags.first(where: { $0.feature.name == id }) completion(.success(flag?.value.stringValue)) - case .failure(let error): + case let .failure(error): if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { completion(.success(flag.value.stringValue)) - } - else { + } else { completion(.failure(error)) } } @@ -177,18 +175,18 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result of `TypedValue` in case of success or `Error` in case of failure public func getValueForFeature(withID id: String, forIdentity identity: String? = nil, - completion: @Sendable @escaping (Result) -> Void) { + completion: @Sendable @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in + getFeatureFlags(forIdentity: identity) { result in switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) + case let .success(flags): + let flag = flags.first(where: { $0.feature.name == id }) completion(.success(flag?.value)) - case .failure(let error): + case let .failure(error): if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { completion(.success(flag.value)) - } - else { + } else { completion(.failure(error)) } } @@ -203,17 +201,18 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result which contains array of Trait objects in case of success or Error in case of failure public func getTraits(withIDS ids: [String]? = nil, forIdentity identity: String, - completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { - getIdentity(identity) { (result) in + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) + { + getIdentity(identity) { result in switch result { - case .success(let identity): + case let .success(identity): if let ids = ids { - let traits = identity.traits.filter({ids.contains($0.key)}) + let traits = identity.traits.filter { ids.contains($0.key) } completion(.success(traits)) } else { completion(.success(identity.traits)) } - case .failure(let error): + case let .failure(error): completion(.failure(error)) } } @@ -227,13 +226,14 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure public func getTrait(withID id: String, forIdentity identity: String, - completion: @Sendable @escaping (Result) -> Void) { - getIdentity(identity) { (result) in + completion: @Sendable @escaping (Result) -> Void) + { + getIdentity(identity) { result in switch result { - case .success(let identity): - let trait = identity.traits.first(where: {$0.key == id}) + case let .success(identity): + let trait = identity.traits.first(where: { $0.key == id }) completion(.success(trait)) - case .failure(let error): + case let .failure(error): completion(.failure(error)) } } @@ -247,7 +247,8 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure public func setTrait(_ trait: Trait, forIdentity identity: String, - completion: @Sendable @escaping (Result) -> Void) { + completion: @Sendable @escaping (Result) -> Void) + { apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in completion(result) } @@ -261,7 +262,8 @@ public final class Flagsmith: @unchecked Sendable { /// - completion: Closure with Result which contains Traits in case of success or Error in case of failure public func setTraits(_ traits: [Trait], forIdentity identity: String, - completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) + { apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in completion(result.map(\.traits)) } @@ -273,22 +275,22 @@ public final class Flagsmith: @unchecked Sendable { /// - identity: ID of the user /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure public func getIdentity(_ identity: String, - completion: @Sendable @escaping (Result) -> Void) { + completion: @Sendable @escaping (Result) -> Void) + { apiManager.request(.getIdentity(identity: identity)) { (result: Result) in completion(result) } } /// Return a flag for a flag ID from the default flags. - private func getFlagUsingDefaults(withID id: String, forIdentity identity: String? = nil) -> Flag? { - return self.defaultFlags.first(where: {$0.feature.name == id}) + private func getFlagUsingDefaults(withID id: String, forIdentity _: String? = nil) -> Flag? { + return defaultFlags.first(where: { $0.feature.name == id }) } } public final class CacheConfig { - /// Cache to use when enabled, defaults to the shared app cache - public var cache: URLCache = URLCache.shared + public var cache: URLCache = .shared /// Use cached flags as a fallback? public var useCache: Bool = false @@ -298,5 +300,4 @@ public final class CacheConfig { /// Skip API if there is a cache available public var skipAPI: Bool = false - } diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index 52ea8d2..67589ec 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -7,16 +7,15 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif /// Handles interaction with a **Flagsmith** api. -final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { - +final class APIManager: NSObject, URLSessionDataDelegate, @unchecked Sendable { private var _session: URLSession! private var session: URLSession { get { - propertiesSerialAccessQueue.sync { _session } + propertiesSerialAccessQueue.sync { _session } } set { propertiesSerialAccessQueue.sync(flags: .barrier) { @@ -29,7 +28,7 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { private var _baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! var baseURL: URL { get { - propertiesSerialAccessQueue.sync { _baseURL } + propertiesSerialAccessQueue.sync { _baseURL } } set { propertiesSerialAccessQueue.sync { @@ -37,11 +36,12 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { } } } + /// API Key unique to an organization. private var _apiKey: String? var apiKey: String? { get { - propertiesSerialAccessQueue.sync { _apiKey } + propertiesSerialAccessQueue.sync { _apiKey } } set { propertiesSerialAccessQueue.sync { @@ -51,25 +51,24 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { } // store the completion handlers and accumulated data for each task - private var tasksToCompletionHandlers:[Int: @Sendable(Result) -> Void] = [:] - private var tasksToData:[Int:Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue", qos: .default) - let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue", qos: .default) + private var tasksToCompletionHandlers: [Int: @Sendable (Result) -> Void] = [:] + private var tasksToData: [Int: Data] = [:] + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue", qos: .default) + let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue", qos: .default) override init() { super.init() let configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) + session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) } - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { serialAccessQueue.sync { if let dataTask = task as? URLSessionDataTask { if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { if let error = error { DispatchQueue.main.async { completion(.failure(FlagsmithError.unhandled(error))) } - } - else { + } else { let data = tasksToData[dataTask.taskIdentifier] ?? Data() DispatchQueue.main.async { completion(.success(data)) } } @@ -80,8 +79,9 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { } } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) { + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) + { serialAccessQueue.sync { // intercept and modify the cache settings for the response if Flagsmith.shared.cacheConfig.useCache { @@ -93,7 +93,7 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { } } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { serialAccessQueue.sync { var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() existingData.append(data) @@ -101,7 +101,9 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { } } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) + { completionHandler(.allow) } @@ -151,7 +153,7 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { request(router) { (result: Result) in switch result { - case .failure(let error): + case let .failure(error): completion(.failure(FlagsmithError(error))) case .success: completion(.success(())) @@ -165,12 +167,14 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { /// - router: The path and parameters that should be requested. /// - decoder: `JSONDecoder` used to deserialize the response data. /// - completion: Function block executed with the result of the request. - func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), completion: @Sendable @escaping (Result) -> Void) { + func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), + completion: @Sendable @escaping (Result) -> Void) + { request(router) { (result: Result) in switch result { - case .failure(let error): + case let .failure(error): completion(.failure(error)) - case .success(let data): + case let .success(data): do { let value = try decoder.decode(T.self, from: data) completion(.success(value)) diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index 1d6c3f0..e5ed65d 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -9,10 +9,9 @@ import Foundation /// Internal analytics for the **FlagsmithClient** final class FlagsmithAnalytics: @unchecked Sendable { - /// Indicates if analytics are enabled. private var _enableAnalytics: Bool = true - var enableAnalytics:Bool { + var enableAnalytics: Bool { get { apiManager.propertiesSerialAccessQueue.sync { _enableAnalytics } } @@ -38,9 +37,9 @@ final class FlagsmithAnalytics: @unchecked Sendable { } private unowned let apiManager: APIManager - private let EVENTS_KEY = "events" - private var _events:[String:Int] = [:] - private var events:[String:Int] { + private let eventsKey = "events" + private var _events: [String: Int] = [:] + private var events: [String: Int] { get { apiManager.propertiesSerialAccessQueue.sync { _events } } @@ -50,8 +49,9 @@ final class FlagsmithAnalytics: @unchecked Sendable { } } } - private var _timer:Timer? - private var timer:Timer? { + + private var _timer: Timer? + private var timer: Timer? { get { apiManager.propertiesSerialAccessQueue.sync { _timer } } @@ -64,12 +64,12 @@ final class FlagsmithAnalytics: @unchecked Sendable { init(apiManager: APIManager) { self.apiManager = apiManager - events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] + events = UserDefaults.standard.dictionary(forKey: eventsKey) as? [String: Int] ?? [:] setupTimer() } /// Counts the instances of a `Flag` being queried. - func trackEvent(flagName:String) { + func trackEvent(flagName: String) { let current = events[flagName] ?? 0 events[flagName] = current + 1 saveEvents() @@ -86,23 +86,23 @@ final class FlagsmithAnalytics: @unchecked Sendable { /// available on 10+. (12.0 would be a good base in the future). private func setupTimer() { timer?.invalidate() -#if canImport(ObjectiveC) - timer = Timer.scheduledTimer( - timeInterval: TimeInterval(flushPeriod), - target: self, - selector: #selector(postAnalyticsWhenEnabled(_:)), - userInfo: nil, - repeats: true - ) -#else - timer = Timer.scheduledTimer( - withTimeInterval: TimeInterval(flushPeriod), - repeats: true, - block: { [weak self] _ in - self?.postAnalytics() - } - ) -#endif + #if canImport(ObjectiveC) + timer = Timer.scheduledTimer( + timeInterval: TimeInterval(flushPeriod), + target: self, + selector: #selector(postAnalyticsWhenEnabled(_:)), + userInfo: nil, + repeats: true + ) + #else + timer = Timer.scheduledTimer( + withTimeInterval: TimeInterval(flushPeriod), + repeats: true, + block: { [weak self] _ in + self?.postAnalytics() + } + ) + #endif } /// Reset events after successful processing. @@ -113,7 +113,7 @@ final class FlagsmithAnalytics: @unchecked Sendable { /// Persist the events to storage. private func saveEvents() { - UserDefaults.standard.set(events, forKey: EVENTS_KEY) + UserDefaults.standard.set(events, forKey: eventsKey) } /// Send analytics to the api when enabled. @@ -136,12 +136,12 @@ final class FlagsmithAnalytics: @unchecked Sendable { } } -#if canImport(ObjectiveC) - /// Event triggered when timer fired. - /// - /// Exposed on Apple platforms to relay selector-based events - @objc private func postAnalyticsWhenEnabled(_ timer: Timer) { - postAnalytics() - } -#endif + #if canImport(ObjectiveC) + /// Event triggered when timer fired. + /// + /// Exposed on Apple platforms to relay selector-based events + @objc private func postAnalyticsWhenEnabled(_: Timer) { + postAnalytics() + } + #endif } diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index eda908e..837e710 100644 --- a/FlagsmithClient/Classes/Trait.swift +++ b/FlagsmithClient/Classes/Trait.swift @@ -8,126 +8,128 @@ import Foundation /** -A Trait represents a value stored against an Identity (user) on the server. -*/ + A Trait represents a value stored against an Identity (user) on the server. + */ public struct Trait: Codable, Sendable { - enum CodingKeys: String, CodingKey { - case key = "trait_key" - case value = "trait_value" - case identity - case identifier - } - - public let key: String - /// The underlying value for the `Trait` - /// - /// - note: In the future, this can be renamed back to 'value' as major/feature-breaking - /// updates are released. - public var typedValue: TypedValue - /// The identity of the `Trait` when creating. - internal let identifier: String? - - public init(key: String, value: TypedValue) { - self.key = key - self.typedValue = value - self.identifier = nil - } - - /// Initializes a `Trait` with an identifier. - /// - /// When a `identifier` is provided, the resulting _encoded_ form of the `Trait` - /// will contain a `identity` key. - internal init(trait: Trait, identifier: String) { - self.key = trait.key - self.typedValue = trait.typedValue - self.identifier = identifier - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - key = try container.decode(String.self, forKey: .key) - typedValue = try container.decode(TypedValue.self, forKey: .value) - identifier = nil - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(key, forKey: .key) - try container.encode(typedValue, forKey: .value) - - if let identifier = identifier { - var identity = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .identity) - try identity.encode(identifier, forKey: .identifier) + enum CodingKeys: String, CodingKey { + case key = "trait_key" + case value = "trait_value" + case identity + case identifier + } + + public let key: String + /// The underlying value for the `Trait` + /// + /// - note: In the future, this can be renamed back to 'value' as major/feature-breaking + /// updates are released. + public var typedValue: TypedValue + /// The identity of the `Trait` when creating. + internal let identifier: String? + + public init(key: String, value: TypedValue) { + self.key = key + typedValue = value + identifier = nil + } + + /// Initializes a `Trait` with an identifier. + /// + /// When a `identifier` is provided, the resulting _encoded_ form of the `Trait` + /// will contain a `identity` key. + internal init(trait: Trait, identifier: String) { + key = trait.key + typedValue = trait.typedValue + self.identifier = identifier + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + key = try container.decode(String.self, forKey: .key) + typedValue = try container.decode(TypedValue.self, forKey: .value) + identifier = nil + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(key, forKey: .key) + try container.encode(typedValue, forKey: .value) + + if let identifier = identifier { + var identity = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .identity) + try identity.encode(identifier, forKey: .identifier) + } } - } } // MARK: - Convenience Initializers + public extension Trait { - init(key: String, value: Bool) { - self.key = key - self.typedValue = .bool(value) - self.identifier = nil - } - - init(key: String, value: Float) { - self.key = key - self.typedValue = .float(value) - self.identifier = nil - } - - init(key: String, value: Int) { - self.key = key - self.typedValue = .int(value) - self.identifier = nil - } - - init(key: String, value: String) { - self.key = key - self.typedValue = .string(value) - self.identifier = nil - } + init(key: String, value: Bool) { + self.key = key + typedValue = .bool(value) + identifier = nil + } + + init(key: String, value: Float) { + self.key = key + typedValue = .float(value) + identifier = nil + } + + init(key: String, value: Int) { + self.key = key + typedValue = .int(value) + identifier = nil + } + + init(key: String, value: String) { + self.key = key + typedValue = .string(value) + identifier = nil + } } // MARK: - Deprecations + public extension Trait { - @available(*, deprecated, renamed: "typedValue") - var value: String { - get { typedValue.description } - set { typedValue = .string(newValue) } - } + @available(*, deprecated, renamed: "typedValue") + var value: String { + get { typedValue.description } + set { typedValue = .string(newValue) } + } } /** -A PostTrait represents a structure to set a new trait, with the Trait fields and the identity. -*/ + A PostTrait represents a structure to set a new trait, with the Trait fields and the identity. + */ @available(*, deprecated) public struct PostTrait: Codable { - enum CodingKeys: String, CodingKey { - case key = "trait_key" - case value = "trait_value" - case identity = "identity" - } - - public let key: String - public var value: String - public var identity: IdentityStruct - - public struct IdentityStruct: Codable { - public var identifier: String - - public enum CodingKeys: String, CodingKey { - case identifier = "identifier" + enum CodingKeys: String, CodingKey { + case key = "trait_key" + case value = "trait_value" + case identity } - - public init(identifier: String) { - self.identifier = identifier + + public let key: String + public var value: String + public var identity: IdentityStruct + + public struct IdentityStruct: Codable { + public var identifier: String + + public enum CodingKeys: String, CodingKey { + case identifier + } + + public init(identifier: String) { + self.identifier = identifier + } + } + + public init(key: String, value: String, identifier: String) { + self.key = key + self.value = value + identity = IdentityStruct(identifier: identifier) } - } - - public init(key: String, value: String, identifier:String) { - self.key = key - self.value = value - self.identity = IdentityStruct(identifier: identifier) - } } diff --git a/FlagsmithClient/Classes/TypedValue.swift b/FlagsmithClient/Classes/TypedValue.swift index b97ef4f..41cd04b 100644 --- a/FlagsmithClient/Classes/TypedValue.swift +++ b/FlagsmithClient/Classes/TypedValue.swift @@ -9,111 +9,111 @@ import Foundation /// A value associated to a `Flag` or `Trait` public enum TypedValue: Equatable, Sendable { - case bool(Bool) - case float(Float) - case int(Int) - case string(String) - case null + case bool(Bool) + case float(Float) + case int(Int) + case string(String) + case null } extension TypedValue: Codable { - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - - if let value = try? container.decode(Bool.self) { - self = .bool(value) - return - } - - if let value = try? container.decode(Int.self) { - self = .int(value) - return - } - - if let value = try? container.decode(Float.self) { - self = .float(value) - return - } - - if let value = try? container.decode(String.self) { - self = .string(value) - return - } - - if container.decodeNil() { - self = .null - return + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + if let value = try? container.decode(Bool.self) { + self = .bool(value) + return + } + + if let value = try? container.decode(Int.self) { + self = .int(value) + return + } + + if let value = try? container.decode(Float.self) { + self = .float(value) + return + } + + if let value = try? container.decode(String.self) { + self = .string(value) + return + } + + if container.decodeNil() { + self = .null + return + } + + let context = DecodingError.Context( + codingPath: [], + debugDescription: "No decodable `TypedValue` value found." + ) + throw DecodingError.valueNotFound((any Decodable).self, context) } - - let context = DecodingError.Context( - codingPath: [], - debugDescription: "No decodable `TypedValue` value found." - ) - throw DecodingError.valueNotFound((any Decodable).self, context) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .bool(let value): - try container.encode(value) - case .float(let value): - try container.encode(value) - case .int(let value): - try container.encode(value) - case .string(let value): - try container.encode(value) - case .null: - try container.encodeNil() + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .bool(value): + try container.encode(value) + case let .float(value): + try container.encode(value) + case let .int(value): + try container.encode(value) + case let .string(value): + try container.encode(value) + case .null: + try container.encodeNil() + } } - } } extension TypedValue: CustomStringConvertible { - public var description: String { - switch self { - case .bool(let value): return "\(value)" - case .float(let value): return "\(value)" - case .int(let value): return "\(value)" - case .string(let value): return value - case .null: return "" + public var description: String { + switch self { + case let .bool(value): return "\(value)" + case let .float(value): return "\(value)" + case let .int(value): return "\(value)" + case let .string(value): return value + case .null: return "" + } } - } } // Provides backwards compatible API for `UnknownTypeValue` // (eg: `Flag.value.intValue?`, `Flag.value.stringValue?`, `Flag.value.floatValue?`) public extension TypedValue { - /// Attempts to cast the associated value as an `Int` - @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") - var intValue: Int? { - switch self { - case .bool(let value): return (value) ? 1 : 0 - case .float(let value): return Int(value) - case .int(let value): return value - case .string(let value): return Int(value) - case .null: return nil + /// Attempts to cast the associated value as an `Int` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var intValue: Int? { + switch self { + case let .bool(value): return value ? 1 : 0 + case let .float(value): return Int(value) + case let .int(value): return value + case let .string(value): return Int(value) + case .null: return nil + } } - } - - /// Attempts to cast the associated value as an `Float` - @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") - var floatValue: Float? { - switch self { - case .bool(let value): return (value) ? 1.0 : 0.0 - case .float(let value): return value - case .int(let value): return Float(value) - case .string(let value): return Float(value) - case .null: return nil + + /// Attempts to cast the associated value as an `Float` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var floatValue: Float? { + switch self { + case let .bool(value): return value ? 1.0 : 0.0 + case let .float(value): return value + case let .int(value): return Float(value) + case let .string(value): return Float(value) + case .null: return nil + } } - } - - /// Attempts to cast the associated value as an `String` - @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") - var stringValue: String? { - switch self { - case .null: return nil - default: return description + + /// Attempts to cast the associated value as an `String` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var stringValue: String? { + switch self { + case .null: return nil + default: return description + } } - } } diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index 083e2f8..2065abf 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -5,85 +5,83 @@ // Created by Richard Piazza on 3/18/22. // -import XCTest @testable import FlagsmithClient +import XCTest final class APIManagerTests: FlagsmithClientTestCase { - let apiManager = APIManager() - + /// Verify that an invalid API key produces the expected error. func testInvalidAPIKey() throws { apiManager.apiKey = nil - + let requestFinished = expectation(description: "Request Finished") - + apiManager.request(.getFlags) { (result: Result) in - if case let .failure(e) = result { - let error = e as? FlagsmithError - let flagsmithError = try! XCTUnwrap(error) - guard case .apiKey = flagsmithError else { + if case let .failure(err) = result { + let error = err as? FlagsmithError + + guard let flagsmithError = try? XCTUnwrap(error), case .apiKey = flagsmithError else { XCTFail("Wrong Error") requestFinished.fulfill() return } } - + requestFinished.fulfill() } - + wait(for: [requestFinished], timeout: 1.0) } - + /// Verify that an invalid API url produces the expected error. func testInvalidAPIURL() throws { apiManager.apiKey = "8D5ABC87-6BBF-4AE7-BC05-4DC1AFE770DF" apiManager.baseURL = URL(fileURLWithPath: "/dev/null") - + let requestFinished = expectation(description: "Request Finished") apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { let error = e as? FlagsmithError - let flagsmithError: FlagsmithError? = try! XCTUnwrap(error) - guard case .apiURL = flagsmithError else { + let flagsmithError: FlagsmithError? = try? XCTUnwrap(error) + guard let flagsmithError = flagsmithError, case .apiURL = flagsmithError else { XCTFail("Wrong Error") requestFinished.fulfill() return } } - + requestFinished.fulfill() } - - wait(for: [requestFinished], timeout: 1.0) + wait(for: [requestFinished], timeout: 1.0) } func testConcurrentRequests() throws { apiManager.apiKey = "8D5ABC87-6BBF-4AE7-BC05-4DC1AFE770DF" let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) - - var expectations:[XCTestExpectation] = []; + + var expectations: [XCTestExpectation] = [] let iterations = 500 - - for concurrentIteration in 1...iterations { + + for concurrentIteration in 1 ... iterations { let expectation = XCTestExpectation(description: "Multiple threads can access the APIManager \(concurrentIteration)") expectations.append(expectation) concurrentQueue.async { self.apiManager.request(.getFlags) { (result: Result) in - if case let .failure(e) = result { - let error = e as? FlagsmithError - // Ensure that we didn't have any errors during the process - XCTAssertTrue(error == nil) - } + if case let .failure(err) = result { + let error = err as? FlagsmithError + // Ensure that we didn't have any errors during the process + XCTAssertTrue(error == nil) + } expectation.fulfill() } } } - + wait(for: expectations, timeout: 10) - + print("Finished!") } }