diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index c7a7054..6d9821d 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -81,14 +81,16 @@ public final class Flagsmith: @unchecked Sendable { /// /// - Parameters: /// - identity: ID of the user (optional) + /// - transient: If `true`, identity is not persisted /// - 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, traits: [Trait]? = nil, + transient: Bool = false, completion: @Sendable @escaping (Result<[Flag], any Error>) -> Void) { if let identity = identity { if let traits = traits { - apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in + apiManager.request(.postTraits(identity: identity, traits: traits, transient: transient)) { (result: Result) in switch result { case let .success(result): completion(.success(result.flags)) @@ -97,7 +99,7 @@ public final class Flagsmith: @unchecked Sendable { } } } else { - getIdentity(identity) { result in + getIdentity(identity, transient: transient) { result in switch result { case let .success(thisIdentity): completion(.success(thisIdentity.flags)) @@ -289,8 +291,10 @@ public final class Flagsmith: @unchecked Sendable { /// /// - Parameters: /// - identity: ID of the user + /// - transient: If `true`, identity is not persisted /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure public func getIdentity(_ identity: String, + transient: Bool = false, completion: @Sendable @escaping (Result) -> Void) { apiManager.request(.getIdentity(identity: identity)) { (result: Result) in diff --git a/FlagsmithClient/Classes/Identity.swift b/FlagsmithClient/Classes/Identity.swift index 0f97830..5e42bcb 100644 --- a/FlagsmithClient/Classes/Identity.swift +++ b/FlagsmithClient/Classes/Identity.swift @@ -8,14 +8,17 @@ import Foundation /** - An Identity represents a user stored on the server. + An `Identity` represents a set of user data used for flag evaluation. + An `Identity` with `transient` set to `true` is not stored in Flagsmith backend. */ public struct Identity: Decodable, Sendable { enum CodingKeys: String, CodingKey { case flags case traits + case transient } public let flags: [Flag] public let traits: [Trait] + public let transient: Bool } diff --git a/FlagsmithClient/Classes/Internal/Router.swift b/FlagsmithClient/Classes/Internal/Router.swift index d8d76a6..eeafcbc 100644 --- a/FlagsmithClient/Classes/Internal/Router.swift +++ b/FlagsmithClient/Classes/Internal/Router.swift @@ -17,9 +17,9 @@ enum Router: Sendable { } case getFlags - case getIdentity(identity: String) + case getIdentity(identity: String, transient: Bool = false) case postTrait(trait: Trait, identity: String) - case postTraits(identity: String, traits: [Trait]) + case postTraits(identity: String, traits: [Trait], transient: Bool = false) case postAnalytics(events: [String: Int]) private var method: HTTPMethod { @@ -46,8 +46,12 @@ enum Router: Sendable { private var parameters: [URLQueryItem]? { switch self { - case let .getIdentity(identity), let .postTraits(identity, _): - return [URLQueryItem(name: "identifier", value: identity)] + case let .getIdentity(identity, transient): + var queryItems = [URLQueryItem(name: "identifier", value: identity)] + if transient { + queryItems.append(URLQueryItem(name: "transient", value: "true")) + } + return queryItems default: return nil } @@ -60,8 +64,8 @@ enum Router: Sendable { 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) + case let .postTraits(identifier, traits, transient): + let traitsWithIdentity = Traits(traits: traits, identifier: identifier, transient: transient) return try encoder.encode(traitsWithIdentity) case let .postAnalytics(events): return try encoder.encode(events) diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 837e710..0ea78c1 100644 --- a/FlagsmithClient/Classes/Trait.swift +++ b/FlagsmithClient/Classes/Trait.swift @@ -8,12 +8,14 @@ import Foundation /** - A Trait represents a value stored against an Identity (user) on the server. + A `Trait` represents a key-value pair used by Flagsmith to segment an `Identity`. + A `Trait` with `transient` set to `true` is not stored in Flagsmith backend. */ public struct Trait: Codable, Sendable { enum CodingKeys: String, CodingKey { case key = "trait_key" case value = "trait_value" + case transient case identity case identifier } @@ -24,11 +26,13 @@ public struct Trait: Codable, Sendable { /// - note: In the future, this can be renamed back to 'value' as major/feature-breaking /// updates are released. public var typedValue: TypedValue + public let transient: Bool /// The identity of the `Trait` when creating. internal let identifier: String? - - public init(key: String, value: TypedValue) { + + public init(key: String, value: TypedValue, transient: Bool = false) { self.key = key + self.transient = transient typedValue = value identifier = nil } @@ -39,12 +43,18 @@ public struct Trait: Codable, Sendable { /// will contain a `identity` key. internal init(trait: Trait, identifier: String) { key = trait.key + transient = trait.transient typedValue = trait.typedValue self.identifier = identifier } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.transient) { + transient = try container.decode(Bool.self, forKey: .transient) + } else { + transient = false + } key = try container.decode(String.self, forKey: .key) typedValue = try container.decode(TypedValue.self, forKey: .value) identifier = nil @@ -56,8 +66,14 @@ public struct Trait: Codable, Sendable { try container.encode(typedValue, forKey: .value) if let identifier = identifier { + // Assume call to `/api/v1/traits` SDK endpoint + // (used to persist traits for previously persisted identities). + // Flagsmith does not process the `transient` attribute in this case, + // so we don't need it here. var identity = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .identity) try identity.encode(identifier, forKey: .identifier) + } else { + try container.encode(transient, forKey: .transient) } } } @@ -65,26 +81,30 @@ public struct Trait: Codable, Sendable { // MARK: - Convenience Initializers public extension Trait { - init(key: String, value: Bool) { + init(key: String, value: Bool, transient: Bool = false) { self.key = key + self.transient = transient typedValue = .bool(value) identifier = nil } - init(key: String, value: Float) { + init(key: String, value: Float, transient: Bool = false) { self.key = key + self.transient = transient typedValue = .float(value) identifier = nil } - init(key: String, value: Int) { + init(key: String, value: Int, transient: Bool = false) { self.key = key + self.transient = transient typedValue = .int(value) identifier = nil } - init(key: String, value: String) { + init(key: String, value: String, transient: Bool = false) { self.key = key + self.transient = transient typedValue = .string(value) identifier = nil } diff --git a/FlagsmithClient/Classes/Traits.swift b/FlagsmithClient/Classes/Traits.swift index 87ee8ec..7001b7c 100644 --- a/FlagsmithClient/Classes/Traits.swift +++ b/FlagsmithClient/Classes/Traits.swift @@ -14,10 +14,19 @@ public struct Traits: Codable, Sendable { public let traits: [Trait] public let identifier: String? public let flags: [Flag] + public let transient: Bool - init(traits: [Trait], identifier: String?, flags: [Flag] = []) { + init(traits: [Trait], identifier: String?, flags: [Flag] = [], transient: Bool? = false) { self.traits = traits self.identifier = identifier self.flags = flags + self.transient = transient ?? false + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(traits, forKey: .traits) + try container.encode(identifier, forKey: .identifier) + try container.encode(transient, forKey: .transient) } } diff --git a/FlagsmithClient/Tests/RouterTests.swift b/FlagsmithClient/Tests/RouterTests.swift index 0d4846f..5b34ec5 100644 --- a/FlagsmithClient/Tests/RouterTests.swift +++ b/FlagsmithClient/Tests/RouterTests.swift @@ -33,6 +33,14 @@ final class RouterTests: FlagsmithClientTestCase { XCTAssertNil(request.httpBody) } + func testGetIdentityRequest_Transient() throws { + let url = try XCTUnwrap(baseUrl) + let route = Router.getIdentity(identity: "6056BCBF", transient: true) + let request = try route.request(baseUrl: url, apiKey: apiKey) + XCTAssertEqual(request.url?.absoluteString, + "https://edge.api.flagsmith.com/api/v1/identities/?identifier=6056BCBF&transient=true") + } + func testPostTraitRequest() throws { let trait = Trait(key: "meaning_of_life", value: 42) let url = try XCTUnwrap(baseUrl) @@ -57,27 +65,47 @@ final class RouterTests: FlagsmithClientTestCase { func testPostTraitsRequest() throws { let questionTrait = Trait(key: "question_meaning_of_life", value: "6 x 9") - let meaningTrait = Trait(key: "meaning_of_life", value: 42) + let meaningTrait = Trait(key: "meaning_of_life", value: 42, transient: true) let url = try XCTUnwrap(baseUrl) let route = Router.postTraits(identity: "A1B2C3D4", traits: [questionTrait, meaningTrait]) 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/identities/?identifier=A1B2C3D4") + XCTAssertEqual(request.url?.absoluteString, "https://edge.api.flagsmith.com/api/v1/identities/") let expectedJson = try """ { "traits" : [ { "trait_key" : "question_meaning_of_life", - "trait_value" : "6 x 9" + "trait_value" : "6 x 9", + "transient": false }, { "trait_key" : "meaning_of_life", - "trait_value" : 42 + "trait_value" : 42, + "transient": true } ], "identifier" : "A1B2C3D4", - "flags": [] + "transient": false + } + """.json(using: .utf8) + let body = try request.httpBody.json() + XCTAssertEqual(body, expectedJson) + } + + func testPostTraitsRequest_TransientIdentity() throws { + let url = try XCTUnwrap(baseUrl) + let route = Router.postTraits(identity: "A1B2C3D4", traits: [], transient: true) + 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/identities/") + + let expectedJson = try """ + { + "traits" : [], + "identifier" : "A1B2C3D4", + "transient": true } """.json(using: .utf8) let body = try request.httpBody.json()