Skip to content

Commit

Permalink
feat: Support transient identities and traits
Browse files Browse the repository at this point in the history
- Add optional `transient` argument to  `getFeatureFlags`
- Add optional `transient` attribute to `Trait` struct
- Remove redundant `identifier` query parameter from `POST /api/v1/identities` call
- Remove unneeded `flags` JSON body key from `POST /api/v1/identities` call
  • Loading branch information
khvn26 committed Oct 7, 2024
1 parent 8763aad commit 95fa8b5
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 22 deletions.
8 changes: 6 additions & 2 deletions FlagsmithClient/Classes/Flagsmith.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Traits, Error>) in
apiManager.request(.postTraits(identity: identity, traits: traits, transient: transient)) { (result: Result<Traits, Error>) in

Check warning on line 93 in FlagsmithClient/Classes/Flagsmith.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Line Length Violation: Line should be 140 characters or less; currently it has 142 characters (line_length)
switch result {
case let .success(result):
completion(.success(result.flags))
Expand All @@ -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))
Expand Down Expand Up @@ -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<Identity, any Error>) -> Void)
{
apiManager.request(.getIdentity(identity: identity)) { (result: Result<Identity, Error>) in
Expand Down
5 changes: 4 additions & 1 deletion FlagsmithClient/Classes/Identity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 10 additions & 6 deletions FlagsmithClient/Classes/Internal/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
34 changes: 27 additions & 7 deletions FlagsmithClient/Classes/Trait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -56,35 +66,45 @@ 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)
}
}
}

// 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
}
Expand Down
11 changes: 10 additions & 1 deletion FlagsmithClient/Classes/Traits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
38 changes: 33 additions & 5 deletions FlagsmithClient/Tests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down

0 comments on commit 95fa8b5

Please sign in to comment.