From 60f07646bdb2f3f22daa8bf3eefecd3f6ff8513f Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Fri, 26 Apr 2024 11:27:41 +0100 Subject: [PATCH 1/8] Add swiftlint --- .github/workflows/pull-request.yml | 15 +++++++++++++++ Package.swift | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index bd5a175..d14dd42 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -27,3 +27,18 @@ jobs: run: swift build -v - name: Run tests run: swift test -v + + swift-lint: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run SwiftLint + uses: norio-nomura/action-swiftlint@3.2.1 + # TODO: enable these settings: + # env: + # DIFF_BASE: ${{ github.base_ref }} + # with: + # args: --strict diff --git a/Package.swift b/Package.swift index 84a8f31..573ef15 100644 --- a/Package.swift +++ b/Package.swift @@ -11,10 +11,15 @@ let package = Package( .target( name: "FlagsmithClient", dependencies: [], - path: "FlagsmithClient/Classes"), + path: "FlagsmithClient/Classes", + plugins: [ + .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]), .testTarget( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], path: "FlagsmithClient/Tests"), + ], + dependencies: [ + .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0") ] ) From dbc6719ff0dd2edefb8e262573cbf71efcce6477 Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Fri, 26 Apr 2024 11:31:34 +0100 Subject: [PATCH 2/8] reorder Package.swift --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 573ef15..2b56322 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,9 @@ let package = Package( products: [ .library(name: "FlagsmithClient", targets: ["FlagsmithClient"]), ], + dependencies: [ + .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0") + ], targets: [ .target( name: "FlagsmithClient", @@ -18,8 +21,5 @@ let package = Package( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], path: "FlagsmithClient/Tests"), - ], - dependencies: [ - .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0") ] ) From 2827e8d7b42ca54a5c796b3ed2cc1f85029d3c18 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 30 Apr 2024 14:10:58 +0100 Subject: [PATCH 3/8] Disable the local swiftlint, add swiftformat, avoid removing the internal keyword from the Trait, format all the files in the SDK --- .swiftformat | 1 + FlagsmithClient/Classes/Feature.swift | 46 +- FlagsmithClient/Classes/Flag.swift | 88 ++-- .../Classes/Flagsmith+Concurrency.swift | 320 ++++++------ FlagsmithClient/Classes/Flagsmith.swift | 480 +++++++++--------- FlagsmithClient/Classes/FlagsmithError.swift | 16 +- FlagsmithClient/Classes/Identity.swift | 18 +- .../Classes/Internal/APIManager.swift | 257 +++++----- .../Classes/Internal/CachedURLResponse.swift | 25 +- .../Classes/Internal/FlagsmithAnalytics.swift | 185 ++++--- FlagsmithClient/Classes/Internal/Router.swift | 166 +++--- FlagsmithClient/Classes/Trait.swift | 212 ++++---- FlagsmithClient/Classes/Traits.swift | 6 +- FlagsmithClient/Classes/TypedValue.swift | 180 +++---- .../Classes/UnknownTypeValue.swift | 33 +- FlagsmithClient/Tests/APIManagerTests.swift | 47 +- FlagsmithClient/Tests/ComparableJson.swift | 4 +- FlagsmithClient/Tests/FlagTests.swift | 14 +- .../Tests/FlagsmithClientTestCase.swift | 3 +- FlagsmithClient/Tests/RouterTests.swift | 23 +- FlagsmithClient/Tests/TraitTests.swift | 17 +- FlagsmithClient/Tests/TypedValueTests.swift | 19 +- Package.resolved | 97 ++++ Package.swift | 15 +- 24 files changed, 1184 insertions(+), 1088 deletions(-) create mode 100644 .swiftformat create mode 100644 Package.resolved diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..a717917 --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--disable redundantInternal diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index 446bb4f..9e244e7 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -11,27 +11,27 @@ import Foundation A Feature represents a flag or remote configuration value on the server. */ public struct Feature: Codable, Sendable { - enum CodingKeys: String, CodingKey { - case name - case type - case description - } - - /// The name of the feature - public let name: String - public let type: String? - public let description: String? - - init(name: String, type: String?, description: String?) { - self.name = name - self.type = type - self.description = description - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.name, forKey: .name) - try container.encodeIfPresent(self.type, forKey: .type) - try container.encodeIfPresent(self.description, forKey: .description) - } + enum CodingKeys: String, CodingKey { + case name + case type + case description + } + + /// The name of the feature + public let name: String + public let type: String? + public let description: String? + + init(name: String, type: String?, description: String?) { + self.name = name + self.type = type + self.description = description + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(type, forKey: .type) + try container.encodeIfPresent(description, forKey: .description) + } } diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index b9a9ebc..27ed2b4 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -8,49 +8,49 @@ 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: 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: 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+Concurrency.swift b/FlagsmithClient/Classes/Flagsmith+Concurrency.swift index 0e0c2f1..20171a3 100644 --- a/FlagsmithClient/Classes/Flagsmith+Concurrency.swift +++ b/FlagsmithClient/Classes/Flagsmith+Concurrency.swift @@ -1,5 +1,5 @@ // -// Flagsmith.swift +// Flagsmith+Concurrency.swift // FlagsmithClient // // Created by Richard Piazza on 3/10/22. @@ -9,173 +9,173 @@ import Foundation @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) public extension Flagsmith { - /// Get all feature flags (flags and remote config) optionally for a specific identity - /// - /// - Parameters: - /// - identity: ID of the user (optional) - /// - returns: Collection of Flag objects - func getFeatureFlags(forIdentity identity: String? = nil) async throws -> [Flag] { - try await withCheckedThrowingContinuation { continuation in - getFeatureFlags(forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + /// Get all feature flags (flags and remote config) optionally for a specific identity + /// + /// - Parameters: + /// - identity: ID of the user (optional) + /// - returns: Collection of Flag objects + func getFeatureFlags(forIdentity identity: String? = nil) async throws -> [Flag] { + try await withCheckedThrowingContinuation { continuation in + getFeatureFlags(forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } } - } - - /// Check feature exists and is enabled optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - returns: Bool value of the feature - func hasFeatureFlag(withID id: String, forIdentity identity: String? = nil) async throws -> Bool { - try await withCheckedThrowingContinuation({ continuation in - hasFeatureFlag(withID: id, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + + /// Check feature exists and is enabled optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - returns: Bool value of the feature + func hasFeatureFlag(withID id: String, forIdentity identity: String? = nil) async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + hasFeatureFlag(withID: id, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } + } + } + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - returns: String value of the feature if available + @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:)") + func getFeatureValue(withID id: String, forIdentity identity: String? = nil) async throws -> String? { + try await withCheckedThrowingContinuation { continuation in + getFeatureValue(withID: id, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - returns: String value of the feature if available - @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:)") - func getFeatureValue(withID id: String, forIdentity identity: String? = nil) async throws -> String? { - try await withCheckedThrowingContinuation({ continuation in - getFeatureValue(withID: id, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + } + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - returns: String value of the feature if available + func getValueForFeature(withID id: String, forIdentity identity: String? = nil) async throws -> TypedValue? { + try await withCheckedThrowingContinuation { continuation in + getValueForFeature(withID: id, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - returns: String value of the feature if available - func getValueForFeature(withID id: String, forIdentity identity: String? = nil) async throws -> TypedValue? { - try await withCheckedThrowingContinuation({ continuation in - getValueForFeature(withID: id, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + } + + /// Get all user traits for provided identity. Optionally filter results with a list of keys + /// + /// - Parameters: + /// - ids: IDs of the trait (optional) + /// - identity: ID of the user + /// - returns: Collection of Trait objects + func getTraits(withIDS ids: [String]? = nil, forIdentity identity: String) async throws -> [Trait] { + try await withCheckedThrowingContinuation { continuation in + getTraits(withIDS: ids, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } - - /// Get all user traits for provided identity. Optionally filter results with a list of keys - /// - /// - Parameters: - /// - ids: IDs of the trait (optional) - /// - identity: ID of the user - /// - returns: Collection of Trait objects - func getTraits(withIDS ids: [String]? = nil, forIdentity identity: String) async throws -> [Trait] { - try await withCheckedThrowingContinuation({ continuation in - getTraits(withIDS: ids, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + } + + /// Get user trait for provided identity and trait key + /// + /// - Parameters: + /// - id: ID of the trait + /// - identity: ID of the user + /// - returns: Optional Trait if found. + func getTrait(withID id: String, forIdentity identity: String) async throws -> Trait? { + try await withCheckedThrowingContinuation { continuation in + getTrait(withID: id, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } - - /// Get user trait for provided identity and trait key - /// - /// - Parameters: - /// - id: ID of the trait - /// - identity: ID of the user - /// - returns: Optional Trait if found. - func getTrait(withID id: String, forIdentity identity: String) async throws -> Trait? { - try await withCheckedThrowingContinuation({ continuation in - getTrait(withID: id, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + } + + /// Set user trait for provided identity + /// + /// - Parameters: + /// - trait: Trait to be created or updated + /// - identity: ID of the user + /// - returns: The Trait requested to be set. + @discardableResult func setTrait(_ trait: Trait, forIdentity identity: String) async throws -> Trait { + try await withCheckedThrowingContinuation { continuation in + setTrait(trait, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } - - /// Set user trait for provided identity - /// - /// - Parameters: - /// - trait: Trait to be created or updated - /// - identity: ID of the user - /// - returns: The Trait requested to be set. - @discardableResult func setTrait(_ trait: Trait, forIdentity identity: String) async throws -> Trait { - try await withCheckedThrowingContinuation({ continuation in - setTrait(trait, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + } + + /// Set user traits in bulk for provided identity + /// + /// - Parameters: + /// - trait: Traits to be created or updated + /// - identity: ID of the user + /// - returns: The Traits requested to be set. + @discardableResult func setTraits(_ traits: [Trait], forIdentity identity: String) async throws -> [Trait] { + try await withCheckedThrowingContinuation { continuation in + setTraits(traits, forIdentity: identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } + } - /// Set user traits in bulk for provided identity - /// - /// - Parameters: - /// - trait: Traits to be created or updated - /// - identity: ID of the user - /// - returns: The Traits requested to be set. - @discardableResult func setTraits(_ traits: [Trait], forIdentity identity: String) async throws -> [Trait] { - try await withCheckedThrowingContinuation({ continuation in - setTraits(traits, forIdentity: identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) - } - } - }) - } - - /// Get both feature flags and user traits for the provided identity - /// - /// - Parameters: - /// - identity: ID of the user - /// - returns: Identity matching the requested ID. - func getIdentity(_ identity: String) async throws -> Identity { - try await withCheckedThrowingContinuation({ continuation in - getIdentity(identity) { result in - switch result { - case .failure(let error): - continuation.resume(throwing: error) - case .success(let value): - continuation.resume(returning: value) + /// Get both feature flags and user traits for the provided identity + /// + /// - Parameters: + /// - identity: ID of the user + /// - returns: Identity matching the requested ID. + func getIdentity(_ identity: String) async throws -> Identity { + try await withCheckedThrowingContinuation { continuation in + getIdentity(identity) { result in + switch result { + case let .failure(error): + continuation.resume(throwing: error) + case let .success(value): + continuation.resume(returning: value) + } + } } - } - }) - } + } } diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 495fff6..8af3cb6 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -7,274 +7,274 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif /// Manage feature flags and remote config across multiple projects, /// environments and organisations. public class Flagsmith { - /// Shared singleton client object - public static let shared = Flagsmith() - private let apiManager = APIManager() - private lazy var analytics = FlagsmithAnalytics(apiManager: apiManager) - - /// Base URL - /// - /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. - public var baseURL: URL { - set { apiManager.baseURL = newValue } - get { apiManager.baseURL } - } - - /// 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 } - } + /// Shared singleton client object + public static let shared = Flagsmith() + private let apiManager = APIManager() + private lazy var analytics = FlagsmithAnalytics(apiManager: apiManager) - /// Is flag analytics enabled? - public var enableAnalytics: Bool { - set { analytics.enableAnalytics = newValue } - get { analytics.enableAnalytics } - } + /// Base URL + /// + /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. + public var baseURL: URL { + set { apiManager.baseURL = newValue } + get { apiManager.baseURL } + } - /// How often to send the flag analytics, in seconds - public var analyticsFlushPeriod: Int { - set { analytics.flushPeriod = newValue } - get { analytics.flushPeriod } - } - - /// Default flags to fall back on if an API call fails - public var defaultFlags: [Flag] = [] - - /// Configuration class for the cache settings - public var cacheConfig:CacheConfig = CacheConfig() + /// 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 } + } - private init() { - } - - /// Get all feature flags (flags and remote config) optionally for a specific identity - /// - /// - Parameters: - /// - 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: @escaping (Result<[Flag], Error>) -> Void) { - - if let identity = identity { - getIdentity(identity) { (result) in - switch result { - case .success(let thisIdentity): - completion(.success(thisIdentity.flags)) - case .failure(let error): - if self.defaultFlags.isEmpty { - completion(.failure(error)) - } - else { - completion(.success(self.defaultFlags)) - } - } - } - } else { - apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in - switch result { - case .success(let flags): - completion(.success(flags)) - case .failure(let error): - if self.defaultFlags.isEmpty { - completion(.failure(error)) - } - else { - completion(.success(self.defaultFlags)) - } - } - } + /// Is flag analytics enabled? + public var enableAnalytics: Bool { + set { analytics.enableAnalytics = newValue } + get { analytics.enableAnalytics } } - } - - /// Check feature exists and is enabled optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - 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: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let 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}) { - completion(.success(true)) - } - else { - completion(.failure(error)) + + /// How often to send the flag analytics, in seconds + public var analyticsFlushPeriod: Int { + set { analytics.flushPeriod = newValue } + get { analytics.flushPeriod } + } + + /// Default flags to fall back on if an API call fails + public var defaultFlags: [Flag] = [] + + /// Configuration class for the cache settings + public var cacheConfig: CacheConfig = .init() + + private init() {} + + /// Get all feature flags (flags and remote config) optionally for a specific identity + /// + /// - Parameters: + /// - 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: @escaping (Result<[Flag], Error>) -> Void) + { + if let identity = identity { + getIdentity(identity) { result in + switch result { + case let .success(thisIdentity): + completion(.success(thisIdentity.flags)) + case let .failure(error): + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } else { + completion(.success(self.defaultFlags)) + } + } + } + } else { + apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in + switch result { + case let .success(flags): + completion(.success(flags)) + case let .failure(error): + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } else { + completion(.success(self.defaultFlags)) + } + } + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result which String in case of success or Error in case of failure - @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") - public func getFeatureValue(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value.stringValue)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value.stringValue)) + + /// Check feature exists and is enabled optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - 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: @escaping (Result) -> Void) + { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { result in + switch result { + case let .success(flags): + let hasFlag = flags.contains(where: { $0.feature.name == id && $0.enabled }) + completion(.success(hasFlag)) + case let .failure(error): + if self.defaultFlags.contains(where: { $0.feature.name == id && $0.enabled }) { + completion(.success(true)) + } else { + completion(.failure(error)) + } + } } - else { - completion(.failure(error)) + } + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result which String in case of success or Error in case of failure + @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") + public func getFeatureValue(withID id: String, + forIdentity identity: String? = nil, + completion: @escaping (Result) -> Void) + { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { result in + switch result { + case let .success(flags): + let flag = flags.first(where: { $0.feature.name == id }) + completion(.success(flag?.value.stringValue)) + case let .failure(error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value.stringValue)) + } else { + completion(.failure(error)) + } + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - 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: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value)) + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - 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: @escaping (Result) -> Void) + { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { result in + switch result { + case let .success(flags): + let flag = flags.first(where: { $0.feature.name == id }) + completion(.success(flag?.value)) + case let .failure(error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value)) + } else { + completion(.failure(error)) + } + } } - else { - completion(.failure(error)) + } + + /// Get all user traits for provided identity. Optionally filter results with a list of keys + /// + /// - Parameters: + /// - ids: IDs of the trait (optional) + /// - identity: ID of the user + /// - 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: @escaping (Result<[Trait], Error>) -> Void) + { + getIdentity(identity) { result in + switch result { + case let .success(identity): + if let ids = ids { + let traits = identity.traits.filter { ids.contains($0.key) } + completion(.success(traits)) + } else { + completion(.success(identity.traits)) + } + case let .failure(error): + completion(.failure(error)) + } } - } } - } - - /// Get all user traits for provided identity. Optionally filter results with a list of keys - /// - /// - Parameters: - /// - ids: IDs of the trait (optional) - /// - identity: ID of the user - /// - 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: @escaping (Result<[Trait], Error>) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - if let ids = ids { - let traits = identity.traits.filter({ids.contains($0.key)}) - completion(.success(traits)) - } else { - completion(.success(identity.traits)) + + /// Get user trait for provided identity and trait key + /// + /// - Parameters: + /// - id: ID of the trait + /// - identity: ID of the user + /// - 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: @escaping (Result) -> Void) + { + getIdentity(identity) { result in + switch result { + case let .success(identity): + let trait = identity.traits.first(where: { $0.key == id }) + completion(.success(trait)) + case let .failure(error): + completion(.failure(error)) + } } - case .failure(let error): - completion(.failure(error)) - } } - } - - /// Get user trait for provided identity and trait key - /// - /// - Parameters: - /// - id: ID of the trait - /// - identity: ID of the user - /// - 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: @escaping (Result) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - let trait = identity.traits.first(where: {$0.key == id}) - completion(.success(trait)) - case .failure(let error): - completion(.failure(error)) - } + + /// Set user trait for provided identity + /// + /// - Parameters: + /// - trait: Trait to be created or updated + /// - identity: ID of the user + /// - 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: @escaping (Result) -> Void) + { + apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Set user trait for provided identity - /// - /// - Parameters: - /// - trait: Trait to be created or updated - /// - identity: ID of the user - /// - 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: @escaping (Result) -> Void) { - apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in - completion(result) + + /// Set user traits in bulk for provided identity + /// + /// - Parameters: + /// - traits: Traits to be created or updated + /// - identity: ID of the user + /// - 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: @escaping (Result<[Trait], Error>) -> Void) + { + apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in + completion(result.map(\.traits)) + } } - } - /// Set user traits in bulk for provided identity - /// - /// - Parameters: - /// - traits: Traits to be created or updated - /// - identity: ID of the user - /// - 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: @escaping (Result<[Trait], Error>) -> Void) { - apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in - completion(result.map(\.traits)) + /// Get both feature flags and user traits for the provided identity + /// + /// - Parameters: + /// - 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: @escaping (Result) -> Void) + { + apiManager.request(.getIdentity(identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Get both feature flags and user traits for the provided identity - /// - /// - Parameters: - /// - 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: @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 _: String? = nil) -> Flag? { + return defaultFlags.first(where: { $0.feature.name == id }) } - } - - /// 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}) - } } public class CacheConfig { + /// Cache to use when enabled, defaults to the shared app cache + public var cache: URLCache = .shared - /// Cache to use when enabled, defaults to the shared app cache - public var cache: URLCache = URLCache.shared - - /// Use cached flags as a fallback? - public var useCache: Bool = false + /// Use cached flags as a fallback? + public var useCache: Bool = false - /// TTL for the cache in seconds, default of 0 means infinite - public var cacheTTL: Double = 0 + /// TTL for the cache in seconds, default of 0 means infinite + public var cacheTTL: Double = 0 - /// Skip API if there is a cache available - public var skipAPI: Bool = false - + /// Skip API if there is a cache available + public var skipAPI: Bool = false } diff --git a/FlagsmithClient/Classes/FlagsmithError.swift b/FlagsmithClient/Classes/FlagsmithError.swift index fda9675..e946b66 100644 --- a/FlagsmithClient/Classes/FlagsmithError.swift +++ b/FlagsmithClient/Classes/FlagsmithError.swift @@ -21,24 +21,24 @@ public enum FlagsmithError: LocalizedError, Sendable { case decoding(DecodingError) /// Unknown or unhandled error was encountered. case unhandled(Error) - + public var errorDescription: String? { switch self { case .apiKey: return "API Key was not provided or invalid" - case .apiURL(let path): + case let .apiURL(path): return "API URL '\(path)' was invalid" - case .encoding(let error): + case let .encoding(error): return "API Request could not be encoded: \(error.localizedDescription)" - case .statusCode(let code): + case let .statusCode(code): return "API Status Code '\(code)' was not expected." - case .decoding(let error): + case let .decoding(error): return "API Response could not be decoded: \(error.localizedDescription)" - case .unhandled(let error): + case let .unhandled(error): return "An unknown or unhandled error was encountered: \(error.localizedDescription)" } } - + /// Initialize a `FlagsmithError` using an existing `Swift.Error`. /// /// The error provided will be processed in several ways: @@ -46,7 +46,7 @@ public enum FlagsmithError: LocalizedError, Sendable { /// * as `EncodingError`: `.encoding()` error will be created. /// * as `DecodingError`: `.decoding()` error will be created. /// * default: `.unhandled()` error will be created. - internal init(_ error: Error) { + init(_ error: Error) { switch error { case let flagsmithError as FlagsmithError: self = flagsmithError diff --git a/FlagsmithClient/Classes/Identity.swift b/FlagsmithClient/Classes/Identity.swift index 1dfbce3..0f97830 100644 --- a/FlagsmithClient/Classes/Identity.swift +++ b/FlagsmithClient/Classes/Identity.swift @@ -8,14 +8,14 @@ import Foundation /** -An Identity represents a user stored on the server. -*/ + An Identity represents a user stored on the server. + */ public struct Identity: Decodable, Sendable { - enum CodingKeys: String, CodingKey { - case flags - case traits - } - - public let flags: [Flag] - public let traits: [Trait] + enum CodingKeys: String, CodingKey { + case flags + case traits + } + + public let flags: [Flag] + public let traits: [Trait] } diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index b13b898..6a3a8e4 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -7,146 +7,145 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif /// Handles interaction with a **Flagsmith** api. -class APIManager : NSObject, URLSessionDataDelegate { - - private var session: URLSession! - - /// Base `URL` used for requests. - var baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! - /// API Key unique to an organization. - var apiKey: String? - - // store the completion handlers and accumulated data for each task - private var tasksToCompletionHandlers:[Int:(Result) -> Void] = [:] - private var tasksToData:[Int:Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") - - override init() { - super.init() - let configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: 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 { - let data = tasksToData[dataTask.taskIdentifier] ?? Data() - DispatchQueue.main.async { completion(.success(data)) } - } +class APIManager: NSObject, URLSessionDataDelegate { + private var session: URLSession! + + /// Base `URL` used for requests. + var baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! + /// API Key unique to an organization. + var apiKey: String? + + // store the completion handlers and accumulated data for each task + private var tasksToCompletionHandlers: [Int: (Result) -> Void] = [:] + private var tasksToData: [Int: Data] = [:] + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") + + override init() { + super.init() + let configuration = URLSessionConfiguration.default + session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) + } + + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: 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 { + let data = tasksToData[dataTask.taskIdentifier] ?? Data() + DispatchQueue.main.async { completion(.success(data)) } + } + } + tasksToCompletionHandlers[dataTask.taskIdentifier] = nil + tasksToData[dataTask.taskIdentifier] = nil + } } - tasksToCompletionHandlers[dataTask.taskIdentifier] = nil - tasksToData[dataTask.taskIdentifier] = nil - } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) { - serialAccessQueue.sync { - // intercept and modify the cache settings for the response - if Flagsmith.shared.cacheConfig.useCache { - let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) - DispatchQueue.main.async { completionHandler(newResponse) } - } else { - DispatchQueue.main.async { completionHandler(proposedResponse) } - } + + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @escaping (CachedURLResponse?) -> Void) + { + serialAccessQueue.sync { + // intercept and modify the cache settings for the response + if Flagsmith.shared.cacheConfig.useCache { + let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) + DispatchQueue.main.async { completionHandler(newResponse) } + } else { + DispatchQueue.main.async { completionHandler(proposedResponse) } + } + } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - serialAccessQueue.sync { - var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() - existingData.append(data) - tasksToData[dataTask.taskIdentifier] = existingData + + func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + serialAccessQueue.sync { + var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() + existingData.append(data) + tasksToData[dataTask.taskIdentifier] = existingData + } } - } - - 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) } - - /// Base request method that handles creating a `URLRequest` and processing - /// the `URLSession` response. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - private func request(_ router: Router, completion: @escaping (Result) -> Void) { - guard let apiKey = apiKey, !apiKey.isEmpty else { - completion(.failure(FlagsmithError.apiKey)) - return - } - - var request: URLRequest - do { - request = try router.request(baseUrl: baseURL, apiKey: apiKey) - } catch { - completion(.failure(error)) - return - } - - // set the cache policy based on Flagsmith settings - request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - session.configuration.urlCache = Flagsmith.shared.cacheConfig.cache - if Flagsmith.shared.cacheConfig.useCache { - request.cachePolicy = .useProtocolCachePolicy - if Flagsmith.shared.cacheConfig.skipAPI { - request.cachePolicy = .returnCacheDataElseLoad - } - } - - // we must use the delegate form here, not the completion handler, to be able to modify the cache - serialAccessQueue.sync { - let task = session.dataTask(with: request) - tasksToCompletionHandlers[task.taskIdentifier] = completion - task.resume() - } - } - - /// Requests a api route and only relays success or failure of the action. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - func request(_ router: Router, completion: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(FlagsmithError(error))) - case .success: - completion(.success(())) - } - } - } - - /// Requests a api route and attempts the decode the response. - /// - /// - parameters: - /// - 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: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let data): + + /// Base request method that handles creating a `URLRequest` and processing + /// the `URLSession` response. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + private func request(_ router: Router, completion: @escaping (Result) -> Void) { + guard let apiKey = apiKey, !apiKey.isEmpty else { + completion(.failure(FlagsmithError.apiKey)) + return + } + + var request: URLRequest do { - let value = try decoder.decode(T.self, from: data) - completion(.success(value)) + request = try router.request(baseUrl: baseURL, apiKey: apiKey) } catch { - completion(.failure(FlagsmithError(error))) + completion(.failure(error)) + return + } + + // set the cache policy based on Flagsmith settings + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + session.configuration.urlCache = Flagsmith.shared.cacheConfig.cache + if Flagsmith.shared.cacheConfig.useCache { + request.cachePolicy = .useProtocolCachePolicy + if Flagsmith.shared.cacheConfig.skipAPI { + request.cachePolicy = .returnCacheDataElseLoad + } + } + + // we must use the delegate form here, not the completion handler, to be able to modify the cache + serialAccessQueue.sync { + let task = session.dataTask(with: request) + tasksToCompletionHandlers[task.taskIdentifier] = completion + task.resume() + } + } + + /// Requests a api route and only relays success or failure of the action. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + func request(_ router: Router, completion: @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case let .failure(error): + completion(.failure(FlagsmithError(error))) + case .success: + completion(.success(())) + } + } + } + + /// Requests a api route and attempts the decode the response. + /// + /// - parameters: + /// - 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: @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case let .failure(error): + completion(.failure(error)) + case let .success(data): + do { + let value = try decoder.decode(T.self, from: data) + completion(.success(value)) + } catch { + completion(.failure(FlagsmithError(error))) + } + } } - } } - } } diff --git a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift index 730fce2..9e13ab2 100644 --- a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift +++ b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift @@ -1,5 +1,5 @@ // -// File.swift +// CachedURLResponse.swift // CachedURLResponse // // Created by Daniel Wichett on 21/06/2023. @@ -7,23 +7,22 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif extension CachedURLResponse { func response(withExpirationDuration duration: Int) -> CachedURLResponse { var cachedResponse = self - if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String : String], let url = httpResponse.url{ - - //set to 1 year (the max allowed) if the value is 0 - headers["Cache-Control"] = "max-age=\(duration == 0 ? 31536000 : duration)" - headers.removeValue(forKey: "Expires") - headers.removeValue(forKey: "s-maxage") + if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String: String], let url = httpResponse.url { + // set to 1 year (the max allowed) if the value is 0 + headers["Cache-Control"] = "max-age=\(duration == 0 ? 31_536_000 : duration)" + headers.removeValue(forKey: "Expires") + headers.removeValue(forKey: "s-maxage") - if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) { - cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy) - } - } - return cachedResponse + if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) { + cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy) + } + } + return cachedResponse } } diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index d172b3b..d803a3e 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -9,101 +9,100 @@ import Foundation /// Internal analytics for the **FlagsmithClient** class FlagsmithAnalytics { - - /// Indicates if analytics are enabled. - var enableAnalytics: Bool = true - /// How often analytics events are processed (in seconds). - var flushPeriod: Int = 10 { - didSet { - setupTimer() + /// Indicates if analytics are enabled. + var enableAnalytics: Bool = true + /// How often analytics events are processed (in seconds). + var flushPeriod: Int = 10 { + didSet { + setupTimer() + } } - } - - private unowned let apiManager: APIManager - private let EVENTS_KEY = "events" - private var events:[String:Int] = [:] - private var timer:Timer? - - init(apiManager: APIManager) { - self.apiManager = apiManager - events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] - setupTimer() - } - - /// Counts the instances of a `Flag` being queried. - func trackEvent(flagName:String) { - let current = events[flagName] ?? 0 - events[flagName] = current + 1 - saveEvents() - } - - /// Invalidate and re-schedule timer for processing events - /// - /// On Apple (Darwin) platforms, this uses the Objective-C based - /// target/selector message sending API. - /// - /// Non-Darwin systems will use the corelibs Foundation block-based - /// api. Both platforms could use this approach, but the podspec - /// declares iOS 8.0 as a minimum target, and that api is only - /// 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 - } - - /// Reset events after successful processing. - private func reset() { - events = [:] - saveEvents() - } - - /// Persist the events to storage. - private func saveEvents() { - UserDefaults.standard.set(events, forKey: EVENTS_KEY) - } - - /// Send analytics to the api when enabled. - private func postAnalytics() { - guard enableAnalytics else { - return + + private unowned let apiManager: APIManager + private let EVENTS_KEY = "events" + private var events: [String: Int] = [:] + private var timer: Timer? + + init(apiManager: APIManager) { + self.apiManager = apiManager + events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String: Int] ?? [:] + setupTimer() } - - guard !events.isEmpty else { - return + + /// Counts the instances of a `Flag` being queried. + func trackEvent(flagName: String) { + let current = events[flagName] ?? 0 + events[flagName] = current + 1 + saveEvents() } - - apiManager.request(.postAnalytics(events: events)) { [weak self] (result: Result) in - switch result { - case .failure: - print("Upload analytics failed") - case .success: - self?.reset() - } + + /// Invalidate and re-schedule timer for processing events + /// + /// On Apple (Darwin) platforms, this uses the Objective-C based + /// target/selector message sending API. + /// + /// Non-Darwin systems will use the corelibs Foundation block-based + /// api. Both platforms could use this approach, but the podspec + /// declares iOS 8.0 as a minimum target, and that api is only + /// 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 + } + + /// Reset events after successful processing. + private func reset() { + events = [:] + saveEvents() } - } - - #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 + + /// Persist the events to storage. + private func saveEvents() { + UserDefaults.standard.set(events, forKey: EVENTS_KEY) + } + + /// Send analytics to the api when enabled. + private func postAnalytics() { + guard enableAnalytics else { + return + } + + guard !events.isEmpty else { + return + } + + apiManager.request(.postAnalytics(events: events)) { [weak self] (result: Result) in + switch result { + case .failure: + print("Upload analytics failed") + case .success: + self?.reset() + } + } + } + + #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/Internal/Router.swift b/FlagsmithClient/Classes/Internal/Router.swift index 639be56..d8d76a6 100644 --- a/FlagsmithClient/Classes/Internal/Router.swift +++ b/FlagsmithClient/Classes/Internal/Router.swift @@ -7,98 +7,98 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking + import FoundationNetworking #endif enum Router: Sendable { - private enum HTTPMethod: String { - case get = "GET" - case post = "POST" - } - - case getFlags - case getIdentity(identity: String) - case postTrait(trait: Trait, identity: String) - case postTraits(identity: String, traits: [Trait]) - case postAnalytics(events: [String:Int]) - - private var method: HTTPMethod { - switch self { - case .getFlags, .getIdentity: - return .get - case .postTrait, .postTraits, .postAnalytics: - return .post + private enum HTTPMethod: String { + case get = "GET" + case post = "POST" } - } - - private var path: String { - switch self { - case .getFlags: - return "flags/" - case .getIdentity, .postTraits: - return "identities/" - case .postTrait: - return "traits/" - case .postAnalytics: - return "analytics/flags/" - } - } - private var parameters: [URLQueryItem]? { - switch self { - case .getIdentity(let identity), .postTraits(let identity, _): - return [URLQueryItem(name: "identifier", value: identity)] - default: - return nil + case getFlags + case getIdentity(identity: String) + case postTrait(trait: Trait, identity: String) + case postTraits(identity: String, traits: [Trait]) + case postAnalytics(events: [String: Int]) + + private var method: HTTPMethod { + switch self { + case .getFlags, .getIdentity: + return .get + case .postTrait, .postTraits, .postAnalytics: + return .post + } } - } - private func body(using encoder: JSONEncoder) throws -> Data? { - switch self { - case .getFlags, .getIdentity: - return nil - case .postTrait(let trait, let identifier): - let traitWithIdentity = Trait(trait: trait, identifier: identifier) - return try encoder.encode(traitWithIdentity) - case .postTraits(let identifier, let traits): - let traitsWithIdentity = Traits(traits: traits, identifier: identifier) - return try encoder.encode(traitsWithIdentity) - case .postAnalytics(let events): - return try encoder.encode(events) + private var path: String { + switch self { + case .getFlags: + return "flags/" + case .getIdentity, .postTraits: + return "identities/" + case .postTrait: + return "traits/" + case .postAnalytics: + return "analytics/flags/" + } } - } - - /// Generate a `URLRequest` with headers and encoded body. - /// - /// - parameters: - /// - baseUrl: The base URL of the api on which to base the request. - /// - apiKey: The organization key to provide in the request headers. - /// - encoder: `JSONEncoder` used to encode the request body. - func request(baseUrl: URL, - apiKey: String, - using encoder: JSONEncoder = JSONEncoder() - ) throws -> URLRequest { - let urlString = baseUrl.appendingPathComponent(path).absoluteString - var urlComponents = URLComponents(string: urlString) - urlComponents?.queryItems = parameters - guard let url = urlComponents?.url else { - // This is unlikely to ever be hit, but it is safer than - // relying on the forcefully-unwrapped optional. - throw FlagsmithError.apiURL(urlString) + + private var parameters: [URLQueryItem]? { + switch self { + case let .getIdentity(identity), let .postTraits(identity, _): + return [URLQueryItem(name: "identifier", value: identity)] + default: + return nil + } } - - guard !url.isFileURL else { - throw FlagsmithError.apiURL(urlString) + + private func body(using encoder: JSONEncoder) throws -> Data? { + switch self { + case .getFlags, .getIdentity: + return nil + case let .postTrait(trait, identifier): + let traitWithIdentity = Trait(trait: trait, identifier: identifier) + return try encoder.encode(traitWithIdentity) + case let .postTraits(identifier, traits): + let traitsWithIdentity = Traits(traits: traits, identifier: identifier) + return try encoder.encode(traitsWithIdentity) + case let .postAnalytics(events): + return try encoder.encode(events) + } } - - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - if let body = try self.body(using: encoder) { - request.httpBody = body + + /// Generate a `URLRequest` with headers and encoded body. + /// + /// - parameters: + /// - baseUrl: The base URL of the api on which to base the request. + /// - apiKey: The organization key to provide in the request headers. + /// - encoder: `JSONEncoder` used to encode the request body. + func request(baseUrl: URL, + apiKey: String, + using encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest + { + let urlString = baseUrl.appendingPathComponent(path).absoluteString + var urlComponents = URLComponents(string: urlString) + urlComponents?.queryItems = parameters + guard let url = urlComponents?.url else { + // This is unlikely to ever be hit, but it is safer than + // relying on the forcefully-unwrapped optional. + throw FlagsmithError.apiURL(urlString) + } + + guard !url.isFileURL else { + throw FlagsmithError.apiURL(urlString) + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + if let body = try body(using: encoder) { + request.httpBody = body + } + request.addValue(apiKey, forHTTPHeaderField: "X-Environment-Key") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + return request } - request.addValue(apiKey, forHTTPHeaderField: "X-Environment-Key") - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - return request - } } diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 85e9ec6..8195915 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: 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: 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: 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: 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/Traits.swift b/FlagsmithClient/Classes/Traits.swift index a6e41cb..da8d4ea 100644 --- a/FlagsmithClient/Classes/Traits.swift +++ b/FlagsmithClient/Classes/Traits.swift @@ -1,5 +1,5 @@ // -// Router.swift +// Traits.swift // FlagsmithClient // // Created by Rob Valdes on 07/02/23. @@ -8,8 +8,8 @@ import Foundation /** -A Traits object represent a collection of different `Trait`s stored against the same Identity (user) on the server. -*/ + A Traits object represent a collection of different `Trait`s stored against the same Identity (user) on the server. + */ public struct Traits: Codable, Sendable { public let traits: [Trait] public let identifier: String? diff --git a/FlagsmithClient/Classes/TypedValue.swift b/FlagsmithClient/Classes/TypedValue.swift index 5bd5794..bb6c223 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: 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: 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(Decodable.self, context) } - - let context = DecodingError.Context( - codingPath: [], - debugDescription: "No decodable `TypedValue` value found." - ) - throw DecodingError.valueNotFound(Decodable.self, context) - } - - public func encode(to encoder: 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: 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/Classes/UnknownTypeValue.swift b/FlagsmithClient/Classes/UnknownTypeValue.swift index 7ff2ed0..aebbb50 100644 --- a/FlagsmithClient/Classes/UnknownTypeValue.swift +++ b/FlagsmithClient/Classes/UnknownTypeValue.swift @@ -8,19 +8,18 @@ import Foundation /** -An UnknownTypeValue represents a value which can have a variable type -*/ + An UnknownTypeValue represents a value which can have a variable type + */ @available(*, deprecated, renamed: "TypedValue") public enum UnknownTypeValue: Decodable, Sendable { - case int(Int), string(String), float(Float), null - + public init(from decoder: Decoder) throws { if let int = try? decoder.singleValueContainer().decode(Int.self) { self = .int(int) return } - + if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) return @@ -33,34 +32,34 @@ public enum UnknownTypeValue: Decodable, Sendable { self = .null } - - public enum UnknownTypeError:Error { + + public enum UnknownTypeError: Error { case missingValue } - + public var intValue: Int? { switch self { - case .int(let value): return value - case .string(let value): return Int(value) - case .float(let value): return Int(value) + case let .int(value): return value + case let .string(value): return Int(value) + case let .float(value): return Int(value) case .null: return nil } } public var stringValue: String? { switch self { - case .int(let value): return String(value) - case .string(let value): return value - case .float(let value): return String(value) + case let .int(value): return String(value) + case let .string(value): return value + case let .float(value): return String(value) case .null: return nil } } public var floatValue: Float? { switch self { - case .int(let value): return Float(value) - case .string(let value): return Float(value) - case .float(let value): return value + case let .int(value): return Float(value) + case let .string(value): return Float(value) + case let .float(value): return value case .null: return nil } } diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index 435e74f..e69b7c4 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -5,87 +5,86 @@ // 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") var error: FlagsmithError? - + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { error = e as? FlagsmithError } - + requestFinished.fulfill() } - + wait(for: [requestFinished], timeout: 1.0) - + let flagsmithError = try XCTUnwrap(error) guard case .apiKey = flagsmithError else { XCTFail("Wrong Error") return } } - + /// 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") var error: FlagsmithError? - + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { error = e as? FlagsmithError } - + requestFinished.fulfill() } - + wait(for: [requestFinished], timeout: 1.0) - + let flagsmithError = try XCTUnwrap(error) guard case .apiURL = flagsmithError else { XCTFail("Wrong Error") return } } - + 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 var error: FlagsmithError? - - 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 { - error = e as? FlagsmithError - } + if case let .failure(e) = result { + error = e as? FlagsmithError + } expectation.fulfill() } } } - + wait(for: expectations, timeout: 10) // Ensure that we didn't have any errors during the process XCTAssertTrue(error == nil) - + print("Finished!") } } diff --git a/FlagsmithClient/Tests/ComparableJson.swift b/FlagsmithClient/Tests/ComparableJson.swift index 15d85a7..c3564f0 100644 --- a/FlagsmithClient/Tests/ComparableJson.swift +++ b/FlagsmithClient/Tests/ComparableJson.swift @@ -9,7 +9,7 @@ import XCTest extension String { func json(using encoding: String.Encoding) throws -> NSDictionary { - return try self.data(using: encoding).json() + return try data(using: encoding).json() } } @@ -23,7 +23,7 @@ extension Optional where Wrapped == Data { extension Data { func json() throws -> NSDictionary { let json = try JSONSerialization.jsonObject(with: self) - let dict = json as! [String : Any] + let dict = json as! [String: Any] return NSDictionary(dictionary: dict) } } diff --git a/FlagsmithClient/Tests/FlagTests.swift b/FlagsmithClient/Tests/FlagTests.swift index 7763778..d60869b 100644 --- a/FlagsmithClient/Tests/FlagTests.swift +++ b/FlagsmithClient/Tests/FlagTests.swift @@ -5,11 +5,10 @@ // Created by Richard Piazza on 3/16/22. // -import XCTest @testable import FlagsmithClient +import XCTest final class FlagTests: FlagsmithClientTestCase { - func testDecodeFlags() throws { let json = """ [ @@ -31,18 +30,17 @@ final class FlagTests: FlagsmithClientTestCase { } ] """ - + let data = try XCTUnwrap(json.data(using: .utf8)) let flags = try decoder.decode([Flag].self, from: data) XCTAssertEqual(flags.count, 2) - - let enabledFlag = try XCTUnwrap(flags.first(where: { $0.enabled } )) + + let enabledFlag = try XCTUnwrap(flags.first(where: { $0.enabled })) XCTAssertEqual(enabledFlag.feature.name, "app_theme") XCTAssertEqual(enabledFlag.value, .int(4)) - - let disabledFlag = try XCTUnwrap(flags.first(where: { !$0.enabled } )) + + let disabledFlag = try XCTUnwrap(flags.first(where: { !$0.enabled })) XCTAssertEqual(disabledFlag.feature.name, "realtime_diagnostics_level") XCTAssertEqual(disabledFlag.value, .string("debug")) } - } diff --git a/FlagsmithClient/Tests/FlagsmithClientTestCase.swift b/FlagsmithClient/Tests/FlagsmithClientTestCase.swift index b247348..8f684f0 100644 --- a/FlagsmithClient/Tests/FlagsmithClientTestCase.swift +++ b/FlagsmithClient/Tests/FlagsmithClientTestCase.swift @@ -5,11 +5,10 @@ // Created by Richard Piazza on 3/16/22. // -import XCTest @testable import FlagsmithClient +import XCTest class FlagsmithClientTestCase: XCTestCase { - let encoder: JSONEncoder = .init() let decoder: JSONDecoder = .init() } diff --git a/FlagsmithClient/Tests/RouterTests.swift b/FlagsmithClient/Tests/RouterTests.swift index 1dcef82..4dc7f64 100644 --- a/FlagsmithClient/Tests/RouterTests.swift +++ b/FlagsmithClient/Tests/RouterTests.swift @@ -5,14 +5,13 @@ // Created by Richard Piazza on 3/21/22. // -import XCTest @testable import FlagsmithClient +import XCTest final class RouterTests: FlagsmithClientTestCase { - let baseUrl = URL(string: "https://edge.api.flagsmith.com/api/v1") let apiKey = "E71DC632-82BA-4522-82F3-D39FB6DC90AC" - + func testGetFlagsRequest() throws { let url = try XCTUnwrap(baseUrl) let route = Router.getFlags @@ -22,7 +21,7 @@ final class RouterTests: FlagsmithClientTestCase { XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "X-Environment-Key" }) ?? false) XCTAssertNil(request.httpBody) } - + func testGetIdentityRequest() throws { let url = try XCTUnwrap(baseUrl) let route = Router.getIdentity(identity: "6056BCBF") @@ -32,7 +31,7 @@ final class RouterTests: FlagsmithClientTestCase { XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "X-Environment-Key" }) ?? false) XCTAssertNil(request.httpBody) } - + func testPostTraitRequest() throws { let trait = Trait(key: "meaning_of_life", value: 42) let url = try XCTUnwrap(baseUrl) @@ -40,7 +39,7 @@ final class RouterTests: FlagsmithClientTestCase { let request = try route.request(baseUrl: url, apiKey: apiKey, using: encoder) XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.url?.absoluteString, "https://edge.api.flagsmith.com/api/v1/traits/") - + let json = try """ { "identity" : { @@ -51,7 +50,7 @@ final class RouterTests: FlagsmithClientTestCase { } """.json(using: .utf8) let body = try request.httpBody.json() - + XCTAssertEqual(body, json) } @@ -82,20 +81,20 @@ final class RouterTests: FlagsmithClientTestCase { let body = try request.httpBody.json() XCTAssertEqual(body, expectedJson) } - + func testPostAnalyticsRequest() throws { let events: [String: Int] = [ "one": 1, - "two": 2 + "two": 2, ] - + let url = try XCTUnwrap(baseUrl) let route = Router.postAnalytics(events: events) let request = try route.request(baseUrl: url, apiKey: apiKey, using: encoder) - + XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.url?.absoluteString, "https://edge.api.flagsmith.com/api/v1/analytics/flags/") - + let json = try """ { "one" : 1, diff --git a/FlagsmithClient/Tests/TraitTests.swift b/FlagsmithClient/Tests/TraitTests.swift index 6b38463..c26bff0 100644 --- a/FlagsmithClient/Tests/TraitTests.swift +++ b/FlagsmithClient/Tests/TraitTests.swift @@ -5,12 +5,11 @@ // Created by Richard Piazza on 3/16/22. // -import XCTest @testable import FlagsmithClient +import XCTest /// Tests `Trait` final class TraitTests: FlagsmithClientTestCase { - func testDecodeTraits() throws { let json = """ [ @@ -36,27 +35,27 @@ final class TraitTests: FlagsmithClientTestCase { } ] """ - + let data = try XCTUnwrap(json.data(using: .utf8)) let traits = try decoder.decode([Trait].self, from: data) XCTAssertEqual(traits.count, 5) - + let boolTrait = try XCTUnwrap(traits.first(where: { $0.key == "is_orange" })) XCTAssertEqual(boolTrait.typedValue, .bool(false)) - + let floatTrait = try XCTUnwrap(traits.first(where: { $0.key == "pi" })) XCTAssertEqual(floatTrait.typedValue, .float(3.14)) - + let intTrait = try XCTUnwrap(traits.first(where: { $0.key == "miles_per_hour" })) XCTAssertEqual(intTrait.typedValue, .int(88)) - + let stringTrait = try XCTUnwrap(traits.first(where: { $0.key == "message" })) XCTAssertEqual(stringTrait.typedValue, .string("Welcome")) - + let nullTrait = try XCTUnwrap(traits.first(where: { $0.key == "deprecated" })) XCTAssertEqual(nullTrait.typedValue, .null) } - + func testEncodeTraits() throws { let wrappedTrait = Trait(key: "dark_mode", value: .bool(true)) let trait = Trait(trait: wrappedTrait, identifier: "theme_settings") diff --git a/FlagsmithClient/Tests/TypedValueTests.swift b/FlagsmithClient/Tests/TypedValueTests.swift index 388c304..a5f60aa 100644 --- a/FlagsmithClient/Tests/TypedValueTests.swift +++ b/FlagsmithClient/Tests/TypedValueTests.swift @@ -5,68 +5,67 @@ // Created by Richard Piazza on 3/16/22. // -import XCTest @testable import FlagsmithClient +import XCTest /// Tests `TypedValue` final class TypedValueTests: FlagsmithClientTestCase { - func testDecodeBool() throws { let json = "true" let data = try XCTUnwrap(json.data(using: .utf8)) let typedValue = try decoder.decode(TypedValue.self, from: data) XCTAssertEqual(typedValue, .bool(true)) } - + func testDecodeFloat() throws { let json = "3.14" let data = try XCTUnwrap(json.data(using: .utf8)) let typedValue = try decoder.decode(TypedValue.self, from: data) XCTAssertEqual(typedValue, .float(3.14)) } - + func testDecodeInt() throws { let json = "47" let data = try XCTUnwrap(json.data(using: .utf8)) let typedValue = try decoder.decode(TypedValue.self, from: data) XCTAssertEqual(typedValue, .int(47)) } - + func testDecodeString() throws { let json = "\"DarkMode\"" let data = try XCTUnwrap(json.data(using: .utf8)) let typedValue = try decoder.decode(TypedValue.self, from: data) XCTAssertEqual(typedValue, .string("DarkMode")) } - + func testDecodeNull() throws { let json = "null" let data = try XCTUnwrap(json.data(using: .utf8)) let typedValue = try decoder.decode(TypedValue.self, from: data) XCTAssertEqual(typedValue, .null) } - + func testEncodeBool() throws { let typedValue: TypedValue = .bool(false) let data = try encoder.encode(typedValue) let json = try XCTUnwrap(String(data: data, encoding: .utf8)) XCTAssertEqual(json, "false") } - + func testEncodeFloat() throws { let typedValue: TypedValue = .float(1.888) let data = try encoder.encode(typedValue) let json = try XCTUnwrap(String(data: data, encoding: .utf8)) XCTAssertTrue(json.hasPrefix("1.888")) } - + func testEncodeInt() throws { let typedValue: TypedValue = .int(88) let data = try encoder.encode(typedValue) let json = try XCTUnwrap(String(data: data, encoding: .utf8)) XCTAssertEqual(json, "88") } - + func testEncodeString() throws { let typedValue: TypedValue = .string("iOS 15.4") let data = try encoder.encode(typedValue) diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..92cd757 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,97 @@ +{ + "object": { + "pins": [ + { + "package": "CollectionConcurrencyKit", + "repositoryURL": "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state": { + "branch": null, + "revision": "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version": "0.2.0" + } + }, + { + "package": "CryptoSwift", + "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "state": { + "branch": null, + "revision": "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version": "1.8.2" + } + }, + { + "package": "SourceKitten", + "repositoryURL": "https://github.com/jpsim/SourceKitten.git", + "state": { + "branch": null, + "revision": "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", + "version": "0.34.1" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version": "1.2.3" + } + }, + { + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", + "state": { + "branch": null, + "revision": "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version": "509.0.2" + } + }, + { + "package": "SwiftFormat", + "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", + "state": { + "branch": null, + "revision": "ab238886b8b50f8b678b251f3c28c0c887305407", + "version": "0.53.8" + } + }, + { + "package": "SwiftLint", + "repositoryURL": "https://github.com/realm/SwiftLint.git", + "state": { + "branch": null, + "revision": "f17a4f9dfb6a6afb0408426354e4180daaf49cee", + "version": "0.54.0" + } + }, + { + "package": "SwiftyTextTable", + "repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git", + "state": { + "branch": null, + "revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version": "0.9.0" + } + }, + { + "package": "SWXMLHash", + "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", + "state": { + "branch": null, + "revision": "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version": "7.0.2" + } + }, + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams.git", + "state": { + "branch": null, + "revision": "9234124cff5e22e178988c18d8b95a8ae8007f76", + "version": "5.1.2" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 2b56322..2d52b66 100644 --- a/Package.swift +++ b/Package.swift @@ -8,18 +8,25 @@ let package = Package( .library(name: "FlagsmithClient", targets: ["FlagsmithClient"]), ], dependencies: [ - .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0") + .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.8"), ], targets: [ .target( name: "FlagsmithClient", dependencies: [], - path: "FlagsmithClient/Classes", - plugins: [ - .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]), + path: "FlagsmithClient/Classes" + // plugins: [ + // .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] + ), .testTarget( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], path: "FlagsmithClient/Tests"), + // .binaryTarget( + // name: "swiftformat", + // url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.53.8/swiftformat.artifactbundle.zip", + // checksum: "12c4cd6e1382479cd38bba63c81eb50121f9b2212a8b1f8f5fa9ed1d1c6d07d1" + // ), ] ) From e98d57be7b2b10de3006422d3ad17b02ac71f6da Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 30 Apr 2024 14:15:24 +0100 Subject: [PATCH 4/8] Restore internal init on FlagsmithError --- FlagsmithClient/Classes/FlagsmithError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlagsmithClient/Classes/FlagsmithError.swift b/FlagsmithClient/Classes/FlagsmithError.swift index e946b66..4f60711 100644 --- a/FlagsmithClient/Classes/FlagsmithError.swift +++ b/FlagsmithClient/Classes/FlagsmithError.swift @@ -46,7 +46,7 @@ public enum FlagsmithError: LocalizedError, Sendable { /// * as `EncodingError`: `.encoding()` error will be created. /// * as `DecodingError`: `.decoding()` error will be created. /// * default: `.unhandled()` error will be created. - init(_ error: Error) { + internal init(_ error: Error) { switch error { case let flagsmithError as FlagsmithError: self = flagsmithError From 4a5d35c483f262c22428bcc54527d228f8edaaab Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Thu, 2 May 2024 09:04:18 +0100 Subject: [PATCH 5/8] Run the linter with --fix to catch a few of the obvious issues --- FlagsmithClient/Classes/Feature.swift | 6 ----- FlagsmithClient/Classes/Flag.swift | 6 +++-- FlagsmithClient/Classes/Flagsmith.swift | 27 +++++++------------ .../Classes/Internal/APIManager.swift | 3 +-- FlagsmithClient/Classes/Internal/Router.swift | 3 +-- FlagsmithClient/Tests/RouterTests.swift | 2 +- 6 files changed, 16 insertions(+), 31 deletions(-) diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index 9e244e7..f4618c9 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -22,12 +22,6 @@ public struct Feature: Codable, Sendable { public let type: String? public let description: String? - init(name: String, type: String?, description: String?) { - self.name = name - self.type = type - self.description = description - } - public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 27ed2b4..07d004a 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -21,8 +21,10 @@ public struct Flag: Codable, Sendable { 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, 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) { diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 8af3cb6..456d702 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -60,8 +60,7 @@ public class Flagsmith { /// - 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: @escaping (Result<[Flag], Error>) -> Void) - { + completion: @escaping (Result<[Flag], Error>) -> Void) { if let identity = identity { getIdentity(identity) { result in switch result { @@ -99,8 +98,7 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -126,8 +124,7 @@ public class Flagsmith { @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") public func getFeatureValue(withID id: String, forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -152,8 +149,7 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -178,8 +174,7 @@ public class Flagsmith { /// - 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: @escaping (Result<[Trait], Error>) -> Void) - { + completion: @escaping (Result<[Trait], Error>) -> Void) { getIdentity(identity) { result in switch result { case let .success(identity): @@ -203,8 +198,7 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { getIdentity(identity) { result in switch result { case let .success(identity): @@ -224,8 +218,7 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in completion(result) } @@ -239,8 +232,7 @@ public class Flagsmith { /// - 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: @escaping (Result<[Trait], Error>) -> Void) - { + completion: @escaping (Result<[Trait], Error>) -> Void) { apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in completion(result.map(\.traits)) } @@ -252,8 +244,7 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) - { + completion: @escaping (Result) -> Void) { apiManager.request(.getIdentity(identity: identity)) { (result: Result) in completion(result) } diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index 6a3a8e4..9d9a246 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -48,8 +48,7 @@ class APIManager: NSObject, URLSessionDataDelegate { } func urlSession(_: URLSession, dataTask _: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) - { + completionHandler: @escaping (CachedURLResponse?) -> Void) { serialAccessQueue.sync { // intercept and modify the cache settings for the response if Flagsmith.shared.cacheConfig.useCache { diff --git a/FlagsmithClient/Classes/Internal/Router.swift b/FlagsmithClient/Classes/Internal/Router.swift index d8d76a6..8c7b545 100644 --- a/FlagsmithClient/Classes/Internal/Router.swift +++ b/FlagsmithClient/Classes/Internal/Router.swift @@ -76,8 +76,7 @@ enum Router: Sendable { /// - encoder: `JSONEncoder` used to encode the request body. func request(baseUrl: URL, apiKey: String, - using encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest - { + using encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest { let urlString = baseUrl.appendingPathComponent(path).absoluteString var urlComponents = URLComponents(string: urlString) urlComponents?.queryItems = parameters diff --git a/FlagsmithClient/Tests/RouterTests.swift b/FlagsmithClient/Tests/RouterTests.swift index 4dc7f64..d955269 100644 --- a/FlagsmithClient/Tests/RouterTests.swift +++ b/FlagsmithClient/Tests/RouterTests.swift @@ -85,7 +85,7 @@ final class RouterTests: FlagsmithClientTestCase { func testPostAnalyticsRequest() throws { let events: [String: Int] = [ "one": 1, - "two": 2, + "two": 2 ] let url = try XCTUnwrap(baseUrl) From fb6e0512008f6ae1573c3509a2e72e18339e842a Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Thu, 2 May 2024 09:22:12 +0100 Subject: [PATCH 6/8] All of the linter errors covered, before running through swiftformat again --- .swiftlint.yml | 15 ++++++ Example/FlagsmithClient/AppDelegate.swift | 53 ++++++++++--------- Example/FlagsmithClient/ViewController.swift | 12 +---- FlagsmithClient/Classes/Flag.swift | 29 ++++++---- FlagsmithClient/Classes/Flagsmith.swift | 8 +-- .../Classes/Internal/APIManager.swift | 6 ++- .../Classes/Internal/CachedURLResponse.swift | 10 ++-- .../Classes/Internal/FlagsmithAnalytics.swift | 7 +-- FlagsmithClient/Tests/APIManagerTests.swift | 12 ++--- FlagsmithClient/Tests/ComparableJson.swift | 7 ++- FlagsmithClient/Tests/RouterTests.swift | 3 +- Package.swift | 6 +-- 12 files changed, 97 insertions(+), 71 deletions(-) create mode 100644 .swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..77d9a04 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,15 @@ +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - .build + +disabled_rules: # rule identifiers to exclude from running + - nesting + - trailing_whitespace + +line_length: + warning: 140 + error: 160 + ignores_comments: true + ignores_urls: true + + \ No newline at end of file diff --git a/Example/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index a68b1d0..d3fc7b5 100644 --- a/Example/FlagsmithClient/AppDelegate.swift +++ b/Example/FlagsmithClient/AppDelegate.swift @@ -9,30 +9,31 @@ import UIKit import FlagsmithClient -func isSuccess(_ result: Result) -> Bool { +func isSuccess(_ result: Result) -> Bool { if case .success = result { return true } else { return false } } @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + var window: UIWindow? let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. Flagsmith.shared.apiKey = "" - + // set default flags Flagsmith.shared.defaultFlags = [Flag(featureName: "feature_a", enabled: false), - Flag(featureName: "font_size", intValue:12, enabled: true), - Flag(featureName: "my_name", stringValue:"Testing", enabled: true)] - + Flag(featureName: "font_size", intValue: 12, enabled: true), + Flag(featureName: "my_name", stringValue: "Testing", enabled: true)] + // set cache on / off (defaults to off) Flagsmith.shared.cacheConfig.useCache = true - + // set custom cache to use (defaults to shared URLCache) - //Flagsmith.shared.cacheConfig.cache = + // Flagsmith.shared.cacheConfig.cache = // set skip API on / off (defaults to off) Flagsmith.shared.cacheConfig.skipAPI = false @@ -42,53 +43,53 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // set analytics on or off Flagsmith.shared.enableAnalytics = true - + // set the analytics flush period in seconds Flagsmith.shared.analyticsFlushPeriod = 10 - - Flagsmith.shared.getFeatureFlags() { (result) in + + Flagsmith.shared.getFeatureFlags { (result) in print(result) } Flagsmith.shared.hasFeatureFlag(withID: "freeze_delinquent_accounts") { (result) in print(result) } - + // Try getting the feature flags concurrently to ensure that this does not cause any issues // This was originally highlighted in https://github.com/Flagsmith/flagsmith-ios-client/pull/40 for _ in 1...20 { concurrentQueue.async { - Flagsmith.shared.getFeatureFlags() { (result) in + Flagsmith.shared.getFeatureFlags { (_) in } } } - - //Flagsmith.shared.setTrait(Trait(key: "", value: ""), forIdentity: "") { (result) in print(result) } - //Flagsmith.shared.getIdentity("") { (result) in print(result) } + + // Flagsmith.shared.setTrait(Trait(key: "", value: ""), forIdentity: "") { (result) in print(result) } + // Flagsmith.shared.getIdentity("") { (result) in print(result) } return true } - + func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } - + func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } - + func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. } - + func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - + func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - + #if swift(>=5.5.2) /// (Example) Setup the app based on the available feature flags. /// @@ -98,7 +99,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @available(iOS 13.0, *) func determineAppConfiguration() async { let flagsmith = Flagsmith.shared - + do { if try await flagsmith.hasFeatureFlag(withID: "ab_test_enabled") { if let theme = try await flagsmith.getValueForFeature(withID: "app_theme") { @@ -116,7 +117,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print(error) } } - + func setTheme(_ theme: TypedValue) {} func processFlags(_ flags: [Flag]) {} #endif diff --git a/Example/FlagsmithClient/ViewController.swift b/Example/FlagsmithClient/ViewController.swift index 0549c97..bd273ba 100644 --- a/Example/FlagsmithClient/ViewController.swift +++ b/Example/FlagsmithClient/ViewController.swift @@ -10,15 +10,5 @@ import UIKit import FlagsmithClient class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - + } diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 07d004a..901f190 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -22,28 +22,37 @@ public struct Flag: Codable, Sendable { public let enabled: Bool public init(featureName: String, boolValue: Bool, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + 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, 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, 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, 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, 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) { + 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 diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 456d702..8e71d99 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -22,28 +22,28 @@ public class Flagsmith { /// /// 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 diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index 9d9a246..4dae331 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -68,7 +68,8 @@ class APIManager: NSObject, URLSessionDataDelegate { } } - func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { completionHandler(.allow) } @@ -132,7 +133,8 @@ class APIManager: NSObject, URLSessionDataDelegate { /// - 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: @escaping (Result) -> Void) { + func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), + completion: @escaping (Result) -> Void) { request(router) { (result: Result) in switch result { case let .failure(error): diff --git a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift index 9e13ab2..862e0a4 100644 --- a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift +++ b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift @@ -13,14 +13,18 @@ import Foundation extension CachedURLResponse { func response(withExpirationDuration duration: Int) -> CachedURLResponse { var cachedResponse = self - if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String: String], let url = httpResponse.url { + if let httpResponse = cachedResponse.response as? HTTPURLResponse, + var headers = httpResponse.allHeaderFields as? [String: String], + let url = httpResponse.url { // set to 1 year (the max allowed) if the value is 0 headers["Cache-Control"] = "max-age=\(duration == 0 ? 31_536_000 : duration)" headers.removeValue(forKey: "Expires") headers.removeValue(forKey: "s-maxage") - if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) { - cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy) + if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, + httpVersion: "HTTP/1.1", headerFields: headers) { + cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, + userInfo: headers, storagePolicy: cachedResponse.storagePolicy) } } return cachedResponse diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index d803a3e..cbe03e8 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -19,13 +19,14 @@ class FlagsmithAnalytics { } private unowned let apiManager: APIManager - private let EVENTS_KEY = "events" + + private let eventsKey = "events" private var events: [String: Int] = [:] private var timer: Timer? 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() } @@ -74,7 +75,7 @@ class FlagsmithAnalytics { /// 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. diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index e69b7c4..c87a39f 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -19,8 +19,8 @@ final class APIManagerTests: FlagsmithClientTestCase { var error: FlagsmithError? apiManager.request(.getFlags) { (result: Result) in - if case let .failure(e) = result { - error = e as? FlagsmithError + if case let .failure(err) = result { + error = err as? FlagsmithError } requestFinished.fulfill() @@ -44,8 +44,8 @@ final class APIManagerTests: FlagsmithClientTestCase { var error: FlagsmithError? apiManager.request(.getFlags) { (result: Result) in - if case let .failure(e) = result { - error = e as? FlagsmithError + if case let .failure(err) = result { + error = err as? FlagsmithError } requestFinished.fulfill() @@ -73,8 +73,8 @@ final class APIManagerTests: FlagsmithClientTestCase { expectations.append(expectation) concurrentQueue.async { self.apiManager.request(.getFlags) { (result: Result) in - if case let .failure(e) = result { - error = e as? FlagsmithError + if case let .failure(err) = result { + error = err as? FlagsmithError } expectation.fulfill() } diff --git a/FlagsmithClient/Tests/ComparableJson.swift b/FlagsmithClient/Tests/ComparableJson.swift index c3564f0..c5ee8e7 100644 --- a/FlagsmithClient/Tests/ComparableJson.swift +++ b/FlagsmithClient/Tests/ComparableJson.swift @@ -23,7 +23,10 @@ extension Optional where Wrapped == Data { extension Data { func json() throws -> NSDictionary { let json = try JSONSerialization.jsonObject(with: self) - let dict = json as! [String: Any] - return NSDictionary(dictionary: dict) + if let dict = json as? [String: Any] { + return NSDictionary(dictionary: dict) + } else { + return NSDictionary() + } } } diff --git a/FlagsmithClient/Tests/RouterTests.swift b/FlagsmithClient/Tests/RouterTests.swift index d955269..8ebd3ef 100644 --- a/FlagsmithClient/Tests/RouterTests.swift +++ b/FlagsmithClient/Tests/RouterTests.swift @@ -27,7 +27,8 @@ final class RouterTests: FlagsmithClientTestCase { let route = Router.getIdentity(identity: "6056BCBF") let request = try route.request(baseUrl: url, apiKey: apiKey) XCTAssertEqual(request.httpMethod, "GET") - XCTAssertEqual(request.url?.absoluteString, "https://edge.api.flagsmith.com/api/v1/identities/?identifier=6056BCBF") + XCTAssertEqual(request.url?.absoluteString, + "https://edge.api.flagsmith.com/api/v1/identities/?identifier=6056BCBF") XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "X-Environment-Key" }) ?? false) XCTAssertNil(request.httpBody) } diff --git a/Package.swift b/Package.swift index 2d52b66..34a07fa 100644 --- a/Package.swift +++ b/Package.swift @@ -5,11 +5,11 @@ import PackageDescription let package = Package( name: "FlagsmithClient", products: [ - .library(name: "FlagsmithClient", targets: ["FlagsmithClient"]), + .library(name: "FlagsmithClient", targets: ["FlagsmithClient"]) ], dependencies: [ .package(url: "https://github.com/realm/SwiftLint.git", from: "0.54.0"), - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.8"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.8") ], targets: [ .target( @@ -22,7 +22,7 @@ let package = Package( .testTarget( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], - path: "FlagsmithClient/Tests"), + path: "FlagsmithClient/Tests") // .binaryTarget( // name: "swiftformat", // url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.53.8/swiftformat.artifactbundle.zip", From 91790bb33842e9e3ab70793349934c809261387f Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Thu, 2 May 2024 09:28:16 +0100 Subject: [PATCH 7/8] More changes, can now run swiftlint and swiftformat together and they're both happy --- .swiftlint.yml | 2 ++ Example/FlagsmithClient/AppDelegate.swift | 2 +- FlagsmithClient/Classes/Flag.swift | 28 +++++++++++-------- FlagsmithClient/Classes/Flagsmith.swift | 27 ++++++++++++------ .../Classes/Internal/APIManager.swift | 9 ++++-- .../Classes/Internal/CachedURLResponse.swift | 8 ++++-- FlagsmithClient/Classes/Internal/Router.swift | 3 +- FlagsmithClient/Tests/RouterTests.swift | 4 +-- 8 files changed, 53 insertions(+), 30 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 77d9a04..94018e1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,6 +5,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. disabled_rules: # rule identifiers to exclude from running - nesting - trailing_whitespace + - opening_brace + - trailing_comma line_length: warning: 140 diff --git a/Example/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index d3fc7b5..16c2daa 100644 --- a/Example/FlagsmithClient/AppDelegate.swift +++ b/Example/FlagsmithClient/AppDelegate.swift @@ -20,7 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. Flagsmith.shared.apiKey = "" diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 901f190..2a9d792 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -22,37 +22,43 @@ public struct Flag: Codable, Sendable { public let enabled: Bool public init(featureName: String, boolValue: Bool, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { self.init(featureName: featureName, value: TypedValue.bool(boolValue), enabled: enabled, - featureType: featureType, featureDescription: featureDescription) + featureType: featureType, featureDescription: featureDescription) } public init(featureName: String, floatValue: Float, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { self.init(featureName: featureName, value: TypedValue.float(floatValue), enabled: enabled, - featureType: featureType, featureDescription: featureDescription) + featureType: featureType, featureDescription: featureDescription) } public init(featureName: String, intValue: Int, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { self.init(featureName: featureName, value: TypedValue.int(intValue), enabled: enabled, - featureType: featureType, featureDescription: featureDescription) + featureType: featureType, featureDescription: featureDescription) } public init(featureName: String, stringValue: String, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { self.init(featureName: featureName, value: TypedValue.string(stringValue), enabled: enabled, - featureType: featureType, featureDescription: featureDescription) + featureType: featureType, featureDescription: featureDescription) } public init(featureName: String, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { self.init(featureName: featureName, value: TypedValue.null, enabled: enabled, - featureType: featureType, featureDescription: featureDescription) + featureType: featureType, featureDescription: featureDescription) } public init(featureName: String, value: TypedValue, enabled: Bool, - featureType: String? = nil, featureDescription: String? = nil) { + featureType: String? = nil, featureDescription: String? = nil) + { feature = Feature(name: featureName, type: featureType, description: featureDescription) self.value = value self.enabled = enabled diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 8e71d99..28331d7 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -60,7 +60,8 @@ public class Flagsmith { /// - 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: @escaping (Result<[Flag], Error>) -> Void) { + completion: @escaping (Result<[Flag], Error>) -> Void) + { if let identity = identity { getIdentity(identity) { result in switch result { @@ -98,7 +99,8 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -124,7 +126,8 @@ public class Flagsmith { @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") public func getFeatureValue(withID id: String, forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -149,7 +152,8 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in switch result { @@ -174,7 +178,8 @@ public class Flagsmith { /// - 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: @escaping (Result<[Trait], Error>) -> Void) { + completion: @escaping (Result<[Trait], Error>) -> Void) + { getIdentity(identity) { result in switch result { case let .success(identity): @@ -198,7 +203,8 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { getIdentity(identity) { result in switch result { case let .success(identity): @@ -218,7 +224,8 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in completion(result) } @@ -232,7 +239,8 @@ public class Flagsmith { /// - 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: @escaping (Result<[Trait], Error>) -> Void) { + completion: @escaping (Result<[Trait], Error>) -> Void) + { apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in completion(result.map(\.traits)) } @@ -244,7 +252,8 @@ public class Flagsmith { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { apiManager.request(.getIdentity(identity: identity)) { (result: Result) in completion(result) } diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index 4dae331..7ae1052 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -48,7 +48,8 @@ class APIManager: NSObject, URLSessionDataDelegate { } func urlSession(_: URLSession, dataTask _: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) { + completionHandler: @escaping (CachedURLResponse?) -> Void) + { serialAccessQueue.sync { // intercept and modify the cache settings for the response if Flagsmith.shared.cacheConfig.useCache { @@ -69,7 +70,8 @@ class APIManager: NSObject, URLSessionDataDelegate { } func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive _: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) + { completionHandler(.allow) } @@ -134,7 +136,8 @@ class APIManager: NSObject, URLSessionDataDelegate { /// - 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: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) + { request(router) { (result: Result) in switch result { case let .failure(error): diff --git a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift index 862e0a4..a724ae3 100644 --- a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift +++ b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift @@ -14,15 +14,17 @@ extension CachedURLResponse { func response(withExpirationDuration duration: Int) -> CachedURLResponse { var cachedResponse = self if let httpResponse = cachedResponse.response as? HTTPURLResponse, - var headers = httpResponse.allHeaderFields as? [String: String], - let url = httpResponse.url { + var headers = httpResponse.allHeaderFields as? [String: String], + let url = httpResponse.url + { // set to 1 year (the max allowed) if the value is 0 headers["Cache-Control"] = "max-age=\(duration == 0 ? 31_536_000 : duration)" headers.removeValue(forKey: "Expires") headers.removeValue(forKey: "s-maxage") if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, - httpVersion: "HTTP/1.1", headerFields: headers) { + httpVersion: "HTTP/1.1", headerFields: headers) + { cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy) } diff --git a/FlagsmithClient/Classes/Internal/Router.swift b/FlagsmithClient/Classes/Internal/Router.swift index 8c7b545..d8d76a6 100644 --- a/FlagsmithClient/Classes/Internal/Router.swift +++ b/FlagsmithClient/Classes/Internal/Router.swift @@ -76,7 +76,8 @@ enum Router: Sendable { /// - encoder: `JSONEncoder` used to encode the request body. func request(baseUrl: URL, apiKey: String, - using encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest { + using encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest + { let urlString = baseUrl.appendingPathComponent(path).absoluteString var urlComponents = URLComponents(string: urlString) urlComponents?.queryItems = parameters diff --git a/FlagsmithClient/Tests/RouterTests.swift b/FlagsmithClient/Tests/RouterTests.swift index 8ebd3ef..45b2382 100644 --- a/FlagsmithClient/Tests/RouterTests.swift +++ b/FlagsmithClient/Tests/RouterTests.swift @@ -28,7 +28,7 @@ final class RouterTests: FlagsmithClientTestCase { let request = try route.request(baseUrl: url, apiKey: apiKey) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url?.absoluteString, - "https://edge.api.flagsmith.com/api/v1/identities/?identifier=6056BCBF") + "https://edge.api.flagsmith.com/api/v1/identities/?identifier=6056BCBF") XCTAssertTrue(request.allHTTPHeaderFields?.contains(where: { $0.key == "X-Environment-Key" }) ?? false) XCTAssertNil(request.httpBody) } @@ -86,7 +86,7 @@ final class RouterTests: FlagsmithClientTestCase { func testPostAnalyticsRequest() throws { let events: [String: Int] = [ "one": 1, - "two": 2 + "two": 2, ] let url = try XCTUnwrap(baseUrl) From 0362a47ff7e5fcb4eb701f6d8bad9a2196d57a52 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Thu, 2 May 2024 09:32:28 +0100 Subject: [PATCH 8/8] Tidy up the Package.swift and add some docs for contributors --- Package.swift | 5 ----- README.md | 10 +++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 34a07fa..f2c4445 100644 --- a/Package.swift +++ b/Package.swift @@ -23,10 +23,5 @@ let package = Package( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], path: "FlagsmithClient/Tests") - // .binaryTarget( - // name: "swiftformat", - // url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.53.8/swiftformat.artifactbundle.zip", - // checksum: "12c4cd6e1382479cd38bba63c81eb50121f9b2212a8b1f8f5fa9ed1d1c6d07d1" - // ), ] ) diff --git a/README.md b/README.md index 9109835..d619a82 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,15 @@ For full documentation visit [https://docs.flagsmith.com/clients/ios/](https://d ## Contributing -Please read [CONTRIBUTING.md](https://gist.github.com/kyle-ssg/c36a03aebe492e45cbd3eefb21cb0486) for details on our code of conduct, and the process for submitting pull requests +Please read [CONTRIBUTING.md](https://gist.github.com/kyle-ssg/c36a03aebe492e45cbd3eefb21cb0486) for details on our code of conduct, and the process for submitting pull requests. + +We use [SwiftLint](https://github.com/realm/SwiftLint) and [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) to keep our code consistent. + +To run `swiftformat` on the project run the following command: + + swift package plugin swiftformat + +To run `swiftlint` on the project please check out the documentation above to install the linter with your preferred method of integration. The linter will run on the project when the PR is raised, so it's worth checking beforehand. ## Getting Help