From aa87afded8bc18e7ceec777b2acc33b02505412c Mon Sep 17 00:00:00 2001 From: Ben Rometsch Date: Thu, 17 Mar 2022 10:10:37 +0000 Subject: [PATCH] Feature/typed values (#18) * 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 --- .github/workflows/pull-request.yml | 18 +++ .gitignore | 1 + Example/FlagsmithClient/AppDelegate.swift | 4 +- Example/Pods/Pods.xcodeproj/project.pbxproj | 4 + FlagsmithClient.podspec | 2 +- FlagsmithClient/Classes/APIManager.swift | 4 +- FlagsmithClient/Classes/Flag.swift | 2 +- .../Classes/Flagsmith+Concurrency.swift | 20 +++ FlagsmithClient/Classes/Flagsmith.swift | 39 ++++-- FlagsmithClient/Classes/Trait.swift | 86 ++++++++++++- FlagsmithClient/Classes/TypedValue.swift | 119 ++++++++++++++++++ .../Classes/UnknownTypeValue.swift | 1 + FlagsmithClient/Tests/FlagTests.swift | 48 +++++++ .../Tests/FlagsmithClientTestCase.swift | 20 +++ FlagsmithClient/Tests/TraitTests.swift | 75 +++++++++++ FlagsmithClient/Tests/TypedValueTests.swift | 76 +++++++++++ Package.swift | 4 + 17 files changed, 502 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/pull-request.yml create mode 100644 FlagsmithClient/Classes/TypedValue.swift create mode 100644 FlagsmithClient/Tests/FlagTests.swift create mode 100644 FlagsmithClient/Tests/FlagsmithClientTestCase.swift create mode 100644 FlagsmithClient/Tests/TraitTests.swift create mode 100644 FlagsmithClient/Tests/TypedValueTests.swift diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..dd60f46 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -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 diff --git a/.gitignore b/.gitignore index 6b33f5d..ca475a8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Example/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index 843ba81..04297f3 100644 --- a/Example/FlagsmithClient/AppDelegate.swift +++ b/Example/FlagsmithClient/AppDelegate.swift @@ -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() @@ -86,7 +86,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - func setTheme(_ theme: String) {} + func setTheme(_ theme: TypedValue) {} func processFlags(_ flags: [Flag]) {} #endif } diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 2d57f77..8bf5b0f 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 9F21DA4355FE6A2AEDDDFFF891DF93B0 /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EFB71AA5556DD77A4882DCEB418E2C /* APIManager.swift */; }; B7C44FEAEB3099494B79EA35B9F71DE0 /* FlagsmithClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C7A4CB83310920D7A2F76B1F4F715BA5 /* FlagsmithClient-dummy.m */; }; C5344C505516395035EAB8B9FC111FE2 /* Pods-FlagsmithClient_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F66D3EFFD8615AE149DCEEE155C049F /* Pods-FlagsmithClient_Example-dummy.m */; }; + E190202E27E24E7600C5EE2C /* TypedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E190202D27E24E7600C5EE2C /* TypedValue.swift */; }; EE4568DD2E22CD3A570CAB24968F9BE7 /* UnknownTypeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772F9BEA66F23FD7DB187ACBEB72BA93 /* UnknownTypeValue.swift */; }; /* End PBXBuildFile section */ @@ -64,6 +65,7 @@ C1CEBBBF5F9F986FB033B56D22422A4A /* Trait.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trait.swift; path = FlagsmithClient/Classes/Trait.swift; sourceTree = ""; }; C55271DDAE55BE2BA1AFA657FA24971A /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; C7A4CB83310920D7A2F76B1F4F715BA5 /* FlagsmithClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FlagsmithClient-dummy.m"; sourceTree = ""; }; + E190202D27E24E7600C5EE2C /* TypedValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TypedValue.swift; path = FlagsmithClient/Classes/TypedValue.swift; sourceTree = ""; }; E28010F1C58E656FC37588C8A00FEE38 /* Pods-FlagsmithClient_Example-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-FlagsmithClient_Example-Info.plist"; sourceTree = ""; }; ED741D6F7C10EB042045149B0E9347C9 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; F69C872C8FED92D20AC2B23547F34B35 /* FlagsmithAnalytics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FlagsmithAnalytics.swift; path = FlagsmithClient/Classes/FlagsmithAnalytics.swift; sourceTree = ""; }; @@ -124,6 +126,7 @@ F69C872C8FED92D20AC2B23547F34B35 /* FlagsmithAnalytics.swift */, 9B3C161B19E351C1591E2039D787AD7C /* Identity.swift */, C1CEBBBF5F9F986FB033B56D22422A4A /* Trait.swift */, + E190202D27E24E7600C5EE2C /* TypedValue.swift */, 772F9BEA66F23FD7DB187ACBEB72BA93 /* UnknownTypeValue.swift */, 6990B5B456D0DD19FB8DF9BFCBB4A363 /* Pod */, 126C2805424EF94E7A836D869A03013D /* Support Files */, @@ -319,6 +322,7 @@ 9F21DA4355FE6A2AEDDDFFF891DF93B0 /* APIManager.swift in Sources */, 0A2426C7E3721ED5AEADF4CC344F6642 /* Feature.swift in Sources */, 4A510C0AE712CD782D25F2276904F827 /* Flag.swift in Sources */, + E190202E27E24E7600C5EE2C /* TypedValue.swift in Sources */, 1845D603B597C651F0EE6080EF459DBB /* Flagsmith.swift in Sources */, 2C55F74AE43F34E4F3952F5B86AADBF5 /* Flagsmith+Concurrency.swift in Sources */, 7191FDB6E8F195E826AE8D6C713F25DE /* FlagsmithAnalytics.swift in Sources */, diff --git a/FlagsmithClient.podspec b/FlagsmithClient.podspec index daf9327..04e389c 100644 --- a/FlagsmithClient.podspec +++ b/FlagsmithClient.podspec @@ -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' } diff --git a/FlagsmithClient/Classes/APIManager.swift b/FlagsmithClient/Classes/APIManager.swift index ed93e63..c7d39b6 100644 --- a/FlagsmithClient/Classes/APIManager.swift +++ b/FlagsmithClient/Classes/APIManager.swift @@ -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 { @@ -123,7 +123,7 @@ class APIManager { self.session = URLSession(configuration: configuration) } - func request(_ router: Router, emptyResponse:Bool = false, completion: @escaping (Result) -> Void) { + func request(_ router: Router, emptyResponse:Bool = false, completion: @escaping (Result) -> Void) { guard let apiKey = apiKey else { fatalError("API Key is missing") } diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 1fee753..7c61c26 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -18,6 +18,6 @@ public struct Flag: Decodable { } public let feature: Feature - public let value: UnknownTypeValue? + public let value: TypedValue public let enabled: Bool } diff --git a/FlagsmithClient/Classes/Flagsmith+Concurrency.swift b/FlagsmithClient/Classes/Flagsmith+Concurrency.swift index 4ff6c23..f6bfe59 100644 --- a/FlagsmithClient/Classes/Flagsmith+Concurrency.swift +++ b/FlagsmithClient/Classes/Flagsmith+Concurrency.swift @@ -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 @@ -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 /// diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index aad1756..bfc12b1 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -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) -> Void) { @@ -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) -> 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 /// @@ -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) -> Void) { - apiManager.request(.postAnalytics(events: FlagsmithAnalytics.shared.events), emptyResponse: true) { (result: Result) 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) -> Void) { + apiManager.request(.postAnalytics(events: FlagsmithAnalytics.shared.events), emptyResponse: true) { (result: Result) in + completion(result) } - + } } diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 738dd9a..8468f2b 100644 --- a/FlagsmithClient/Classes/Trait.swift +++ b/FlagsmithClient/Classes/Trait.swift @@ -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" @@ -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 diff --git a/FlagsmithClient/Classes/TypedValue.swift b/FlagsmithClient/Classes/TypedValue.swift new file mode 100644 index 0000000..dd95f80 --- /dev/null +++ b/FlagsmithClient/Classes/TypedValue.swift @@ -0,0 +1,119 @@ +// +// TypedValue.swift +// FlagsmithClient +// +// Created by Richard Piazza on 3/16/22. +// + +import Foundation + +/// A value associated to a `Flag` or `Trait` +public enum TypedValue: Equatable { + case bool(Bool) + case float(Float) + case int(Int) + case string(String) + case null +} + +extension TypedValue: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let value = try? container.decode(Bool.self) { + self = .bool(value) + return + } + + if let value = try? container.decode(Int.self) { + self = .int(value) + return + } + + if let value = try? container.decode(Float.self) { + self = .float(value) + return + } + + if let value = try? container.decode(String.self) { + self = .string(value) + return + } + + if container.decodeNil() { + self = .null + return + } + + let context = DecodingError.Context( + codingPath: [], + debugDescription: "No decodable `TypedValue` value found." + ) + throw DecodingError.valueNotFound(Decodable.self, context) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .bool(let value): + try container.encode(value) + case .float(let value): + try container.encode(value) + case .int(let value): + try container.encode(value) + case .string(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} + +extension TypedValue: CustomStringConvertible { + public var description: String { + switch self { + case .bool(let value): return "\(value)" + case .float(let value): return "\(value)" + case .int(let value): return "\(value)" + case .string(let value): return value + case .null: return "" + } + } +} + +// Provides backwards compatible API for `UnknownTypeValue` +// (eg: `Flag.value.intValue?`, `Flag.value.stringValue?`, `Flag.value.floatValue?`) +public extension TypedValue { + /// Attempts to cast the associated value as an `Int` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var intValue: Int? { + switch self { + case .bool(let value): return (value) ? 1 : 0 + case .float(let value): return Int(value) + case .int(let value): return value + case .string(let value): return Int(value) + case .null: return nil + } + } + + /// Attempts to cast the associated value as an `Float` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var floatValue: Float? { + switch self { + case .bool(let value): return (value) ? 1.0 : 0.0 + case .float(let value): return value + case .int(let value): return Float(value) + case .string(let value): return Float(value) + case .null: return nil + } + } + + /// Attempts to cast the associated value as an `String` + @available(*, deprecated, message: "Switch on `TypedValue` to retrieve the associated data type.") + var stringValue: String? { + switch self { + case .null: return nil + default: return description + } + } +} diff --git a/FlagsmithClient/Classes/UnknownTypeValue.swift b/FlagsmithClient/Classes/UnknownTypeValue.swift index cd747be..0ed1d7e 100644 --- a/FlagsmithClient/Classes/UnknownTypeValue.swift +++ b/FlagsmithClient/Classes/UnknownTypeValue.swift @@ -10,6 +10,7 @@ import Foundation /** An UnknownTypeValue represents a value which can have a variable type */ +@available(*, deprecated, renamed: "TypedValue") public enum UnknownTypeValue: Decodable { case int(Int), string(String), float(Float), null diff --git a/FlagsmithClient/Tests/FlagTests.swift b/FlagsmithClient/Tests/FlagTests.swift new file mode 100644 index 0000000..7763778 --- /dev/null +++ b/FlagsmithClient/Tests/FlagTests.swift @@ -0,0 +1,48 @@ +// +// FlagTests.swift +// FlagsmithClientTests +// +// Created by Richard Piazza on 3/16/22. +// + +import XCTest +@testable import FlagsmithClient + +final class FlagTests: FlagsmithClientTestCase { + + func testDecodeFlags() throws { + let json = """ + [ + { + "feature": { + "name": "app_theme", + "type": null, + "description": \"\" + }, + "feature_state_value": 4, + "enabled": true + }, + { + "feature": { + "name": "realtime_diagnostics_level" + }, + "feature_state_value": "debug", + "enabled": false + } + ] + """ + + let data = try XCTUnwrap(json.data(using: .utf8)) + let flags = try decoder.decode([Flag].self, from: data) + XCTAssertEqual(flags.count, 2) + + let enabledFlag = try XCTUnwrap(flags.first(where: { $0.enabled } )) + XCTAssertEqual(enabledFlag.feature.name, "app_theme") + XCTAssertEqual(enabledFlag.value, .int(4)) + + let disabledFlag = try XCTUnwrap(flags.first(where: { !$0.enabled } )) + XCTAssertEqual(disabledFlag.feature.name, "realtime_diagnostics_level") + XCTAssertEqual(disabledFlag.value, .string("debug")) + } + +} diff --git a/FlagsmithClient/Tests/FlagsmithClientTestCase.swift b/FlagsmithClient/Tests/FlagsmithClientTestCase.swift new file mode 100644 index 0000000..f8d9872 --- /dev/null +++ b/FlagsmithClient/Tests/FlagsmithClientTestCase.swift @@ -0,0 +1,20 @@ +// +// FlagsmithClientTestCase.swift +// FlagsmithClientTests +// +// Created by Richard Piazza on 3/16/22. +// + +import XCTest +@testable import FlagsmithClient + +class FlagsmithClientTestCase: XCTestCase { + + let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + return encoder + }() + + let decoder: JSONDecoder = .init() +} diff --git a/FlagsmithClient/Tests/TraitTests.swift b/FlagsmithClient/Tests/TraitTests.swift new file mode 100644 index 0000000..a991776 --- /dev/null +++ b/FlagsmithClient/Tests/TraitTests.swift @@ -0,0 +1,75 @@ +// +// TraitTests.swift +// FlagsmithClientTests +// +// Created by Richard Piazza on 3/16/22. +// + +import XCTest +@testable import FlagsmithClient + +/// Tests `Trait` +final class TraitTests: FlagsmithClientTestCase { + + func testDecodeTraits() throws { + let json = """ + [ + { + "trait_key": "is_orange", + "trait_value": false + }, + { + "trait_key": "pi", + "trait_value": 3.14 + }, + { + "trait_key": "miles_per_hour", + "trait_value": 88 + }, + { + "trait_key": "message", + "trait_value": "Welcome" + }, + { + "trait_key": "deprecated", + "trait_value": null + } + ] + """ + + let data = try XCTUnwrap(json.data(using: .utf8)) + let traits = try decoder.decode([Trait].self, from: data) + XCTAssertEqual(traits.count, 5) + + let boolTrait = try XCTUnwrap(traits.first(where: { $0.key == "is_orange" })) + XCTAssertEqual(boolTrait.typedValue, .bool(false)) + + let floatTrait = try XCTUnwrap(traits.first(where: { $0.key == "pi" })) + XCTAssertEqual(floatTrait.typedValue, .float(3.14)) + + let intTrait = try XCTUnwrap(traits.first(where: { $0.key == "miles_per_hour" })) + XCTAssertEqual(intTrait.typedValue, .int(88)) + + let stringTrait = try XCTUnwrap(traits.first(where: { $0.key == "message" })) + XCTAssertEqual(stringTrait.typedValue, .string("Welcome")) + + let nullTrait = try XCTUnwrap(traits.first(where: { $0.key == "deprecated" })) + XCTAssertEqual(nullTrait.typedValue, .null) + } + + func testEncodeTraits() throws { + let wrappedTrait = Trait(key: "dark_mode", value: .bool(true)) + let trait = Trait(trait: wrappedTrait, identifier: "theme_settings") + let data = try encoder.encode(trait) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(json, """ + { + "identity" : { + "identifier" : "theme_settings" + }, + "trait_key" : "dark_mode", + "trait_value" : true + } + """) + } +} diff --git a/FlagsmithClient/Tests/TypedValueTests.swift b/FlagsmithClient/Tests/TypedValueTests.swift new file mode 100644 index 0000000..388c304 --- /dev/null +++ b/FlagsmithClient/Tests/TypedValueTests.swift @@ -0,0 +1,76 @@ +// +// TypedValueTests.swift +// FlagsmithClient +// +// Created by Richard Piazza on 3/16/22. +// + +import XCTest +@testable import FlagsmithClient + +/// Tests `TypedValue` +final class TypedValueTests: FlagsmithClientTestCase { + + func testDecodeBool() throws { + let json = "true" + let data = try XCTUnwrap(json.data(using: .utf8)) + let typedValue = try decoder.decode(TypedValue.self, from: data) + XCTAssertEqual(typedValue, .bool(true)) + } + + func testDecodeFloat() throws { + let json = "3.14" + let data = try XCTUnwrap(json.data(using: .utf8)) + let typedValue = try decoder.decode(TypedValue.self, from: data) + XCTAssertEqual(typedValue, .float(3.14)) + } + + func testDecodeInt() throws { + let json = "47" + let data = try XCTUnwrap(json.data(using: .utf8)) + let typedValue = try decoder.decode(TypedValue.self, from: data) + XCTAssertEqual(typedValue, .int(47)) + } + + func testDecodeString() throws { + let json = "\"DarkMode\"" + let data = try XCTUnwrap(json.data(using: .utf8)) + let typedValue = try decoder.decode(TypedValue.self, from: data) + XCTAssertEqual(typedValue, .string("DarkMode")) + } + + func testDecodeNull() throws { + let json = "null" + let data = try XCTUnwrap(json.data(using: .utf8)) + let typedValue = try decoder.decode(TypedValue.self, from: data) + XCTAssertEqual(typedValue, .null) + } + + func testEncodeBool() throws { + let typedValue: TypedValue = .bool(false) + let data = try encoder.encode(typedValue) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(json, "false") + } + + func testEncodeFloat() throws { + let typedValue: TypedValue = .float(1.888) + let data = try encoder.encode(typedValue) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertTrue(json.hasPrefix("1.888")) + } + + func testEncodeInt() throws { + let typedValue: TypedValue = .int(88) + let data = try encoder.encode(typedValue) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(json, "88") + } + + func testEncodeString() throws { + let typedValue: TypedValue = .string("iOS 15.4") + let data = try encoder.encode(typedValue) + let json = try XCTUnwrap(String(data: data, encoding: .utf8)) + XCTAssertEqual(json, "\"iOS 15.4\"") + } +} diff --git a/Package.swift b/Package.swift index 965d718..e37a349 100644 --- a/Package.swift +++ b/Package.swift @@ -12,5 +12,9 @@ let package = Package( name: "FlagsmithClient", dependencies: [], path: "FlagsmithClient/Classes"), + .testTarget( + name: "FlagsmitClientTests", + dependencies: ["FlagsmithClient"], + path: "FlagsmithClient/Tests"), ] )