Skip to content

Commit

Permalink
Merge branch 'main' into feature/sse-support
Browse files Browse the repository at this point in the history
# Conflicts:
#	Example/FlagsmithClient/Base.lproj/LaunchScreen.xib
#	FlagsmithClient/Classes/Traits.swift
  • Loading branch information
gazreese committed Oct 11, 2024
2 parents 0345cde + ba0fb2b commit df9ef08
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 26 deletions.
11 changes: 8 additions & 3 deletions Example/FlagsmithClient/Base.lproj/LaunchScreen.xib
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand All @@ -13,6 +12,12 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="© 2024 Flagsmith. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="440" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="FlagsmithClient" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="139.66666666666666" width="440" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
Expand Down
10 changes: 8 additions & 2 deletions FlagsmithClient/Classes/Flagsmith.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,19 @@ 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)
{
lastUsedIdentity = identity
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
switch result {
case let .success(result):
completion(.success(result.flags))
Expand All @@ -136,7 +140,7 @@ public final class Flagsmith: @unchecked Sendable {
}
}
} else {
getIdentity(identity) { result in
getIdentity(identity, transient: transient) { result in
switch result {
case let .success(thisIdentity):
self.updateFlagStreamAndLastUpdatedAt(thisIdentity.flags)
Expand Down Expand Up @@ -331,8 +335,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
13 changes: 11 additions & 2 deletions 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]

init(traits: [Trait], identifier: String?, flags: [Flag] = []) {
public let transient: Bool

init(traits: [Trait], identifier: String?, flags: [Flag] = [], transient: Bool = false) {
self.traits = traits
self.identifier = identifier
self.flags = flags
self.transient = transient
}

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 df9ef08

Please sign in to comment.