Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/typed values #18

Merged
merged 8 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Pull Request Build and Test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
macos-build:
runs-on: macos-latest

steps:
- uses: actions/checkout@v2
- name: Build (macOS)
run: swift build -v
- name: Run tests
run: swift test -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ Carthage/Build
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm
.build
4 changes: 2 additions & 2 deletions Example/FlagsmithClient/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

do {
if try await flagsmith.hasFeatureFlag(withID: "ab_test_enabled") {
if let theme = try await flagsmith.getFeatureValue(withID: "app_theme") {
if let theme = try await flagsmith.getValueForFeature(withID: "app_theme") {
setTheme(theme)
} else {
let flags = try await flagsmith.getFeatureFlags()
Expand All @@ -86,7 +86,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

func setTheme(_ theme: String) {}
func setTheme(_ theme: TypedValue) {}
func processFlags(_ flags: [Flag]) {}
#endif
}
4 changes: 4 additions & 0 deletions Example/Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion FlagsmithClient.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |s|
s.name = 'FlagsmithClient'
s.version = '1.1.2'
s.version = '1.2.0'
s.summary = 'iOS Client written in Swift for Flagsmith. Ship features with confidence using feature flags and remote config.'
s.homepage = 'https://github.com/Flagsmith/flagsmith-ios-client'
s.license = { :type => 'MIT', :file => 'LICENSE' }
Expand Down
4 changes: 2 additions & 2 deletions FlagsmithClient/Classes/APIManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ enum Router {
return .success(nil)
case .postTrait(let trait, let identifier):
do {
let postTraitStruct = PostTrait(key:trait.key, value:trait.value, identifier:identifier)
let postTraitStruct = Trait(trait: trait, identifier: identifier)
let json = try JSONEncoder().encode(postTraitStruct)
return .success(json)
} catch {
Expand Down Expand Up @@ -123,7 +123,7 @@ class APIManager {
self.session = URLSession(configuration: configuration)
}

func request<T: Decodable>(_ router: Router, emptyResponse:Bool = false, completion: @escaping (Result<T, Error>) -> Void) {
func request<T: Decodable>(_ router: Router, emptyResponse:Bool = false, completion: @escaping (Result<T, Error>) -> Void) {
guard let apiKey = apiKey else {
fatalError("API Key is missing")
}
Expand Down
2 changes: 1 addition & 1 deletion FlagsmithClient/Classes/Flag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ public struct Flag: Decodable {
}

public let feature: Feature
public let value: UnknownTypeValue?
public let value: TypedValue
public let enabled: Bool
}
20 changes: 20 additions & 0 deletions FlagsmithClient/Classes/Flagsmith+Concurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public extension Flagsmith {
/// - 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
Expand All @@ -65,6 +66,25 @@ public extension Flagsmith {
}
})
}

/// 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
///
Expand Down
39 changes: 30 additions & 9 deletions FlagsmithClient/Classes/Flagsmith.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public class Flagsmith {
/// - 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<String?, Error>) -> Void) {
Expand All @@ -108,6 +109,27 @@ public class Flagsmith {
}
}
}

/// 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<TypedValue?, Error>) -> Void) {
FlagsmithAnalytics.shared.trackEvent(flagName: id)
getFeatureFlags(forIdentity: identity) { (result) in
switch result {
case .success(let flags):
let value = flags.first(where: {$0.feature.name == id})?.value
completion(.success(value))
case .failure(let error):
completion(.failure(error))
}
}
}

/// Get all user traits for provided identity. Optionally filter results with a list of keys
///
Expand Down Expand Up @@ -179,14 +201,13 @@ public class Flagsmith {
}
}

/// Post analytics
///
/// - Parameters:
/// - completion: Closure with Result which contains empty String in case of success or Error in case of failure
func postAnalytics(completion: @escaping (Result<String, Error>) -> Void) {
apiManager.request(.postAnalytics(events: FlagsmithAnalytics.shared.events), emptyResponse: true) { (result: Result<String, Error>) in
completion(result)
}
/// Post analytics
///
/// - Parameters:
/// - completion: Closure with Result which contains empty String in case of success or Error in case of failure
func postAnalytics(completion: @escaping (Result<String, Error>) -> Void) {
apiManager.request(.postAnalytics(events: FlagsmithAnalytics.shared.events), emptyResponse: true) { (result: Result<String, Error>) in
completion(result)
}

}
}
86 changes: 80 additions & 6 deletions FlagsmithClient/Classes/Trait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,94 @@ public struct Trait: Codable {
enum CodingKeys: String, CodingKey {
case key = "trait_key"
case value = "trait_value"
case identity
case identifier
}

public let key: String
public var value: 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: String) {
public init(key: String, value: TypedValue) {
self.key = key
self.value = value
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)
}
}
}

// 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
}
}

// MARK: - Deprecations
public extension Trait {
@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.
*/
@available(*, deprecated)
public struct PostTrait: Codable {
enum CodingKeys: String, CodingKey {
case key = "trait_key"
Expand All @@ -41,16 +115,16 @@ public struct PostTrait: Codable {

public struct IdentityStruct: Codable {
public var identifier: String

public enum CodingKeys: String, CodingKey {
case identifier = "identifier"
case identifier = "identifier"
}

public init(identifier: String) {
self.identifier = identifier
}
}

public init(key: String, value: String, identifier:String) {
self.key = key
self.value = value
Expand Down
Loading