Skip to content

Commit

Permalink
Feature/typed values (#18)
Browse files Browse the repository at this point in the history
* TypeValue (Flags & Traits)

* Added '.build' directory to Git Ignore (swift build)

* Additional line indentation cleanup & TypedValue/Trait convenience methods

* Restored 'getFeatureValue' to using the Flag 'stringValue'

* Version bump

* Github action

* Formatting

* remove ubuntu from tests

Co-authored-by: Richard Piazza <[email protected]>
  • Loading branch information
dabeeeenster and richardpiazza authored Mar 17, 2022
1 parent 47c1f16 commit aa87afd
Show file tree
Hide file tree
Showing 17 changed files with 502 additions and 21 deletions.
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

0 comments on commit aa87afd

Please sign in to comment.