From 8de4b7805378866023e939aec39c71a78ba771fe Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 9 Apr 2024 10:36:56 +0200 Subject: [PATCH] feat: Add ConfidenceValue (#84) * Add ConfidenceValue * Add ConfidenceValueTests * Remove DateComponents * Simplify and finalize ConfidenceValue * Test fix * Update demo app * Variable renaming * ConfidenceValue is only Codable * Add Confidence-OF converstions * refactor: Restrict heterogeneous ConfidenceValue lists (#86) * Change ConfidenceValue constructors for more control * Setup for list restrictions * Remove unused converters * Change time-related arg labels * Fix date format * Explicit UTC settings and TZ tests * Best effort convert OF lists * Rename bool to boolean * Rename TZ offset from timestamps * Add swiftpm back --- .gitignore | 3 +- .../xcschemes/Confidence-Package.xcscheme | 123 ++++++++ .../xcschemes/Confidence.xcscheme | 66 +++++ .../xcschemes/ConfidenceProvider.xcscheme | 92 ++++++ .../ConfidenceDemoApp/ConfidenceDemoApp.swift | 2 +- Package.swift | 8 +- Sources/Confidence/Confidence.swift | 10 +- .../ConfidenceError.swift | 0 .../Confidence/ConfidenceEventSender.swift | 3 +- Sources/Confidence/ConfidenceValue.swift | 272 ++++++++++++++++++ Sources/Confidence/Contextual.swift | 7 +- .../Apply/FlagApplierWithRetries.swift | 1 + .../Cache/DefaultStorage.swift | 1 + .../Cache/InMemoryProviderCache.swift | 3 +- .../LocalStorageResolver.swift | 1 + .../Utils/ConfidenceTypeMapper.swift | 67 +++++ .../Utils/HttpStatusCode+Error.swift | 1 + .../ConfidenceTypeMapperTest.swift | 99 +++++++ .../Helpers/AlwaysFailCache.swift | 1 + .../LocalStorageResolverTest.swift | 1 + .../ConfidenceValueTests.swift | 143 +++++++++ 21 files changed, 888 insertions(+), 16 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Confidence-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Confidence.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ConfidenceProvider.xcscheme rename Sources/{ConfidenceProvider => Confidence}/ConfidenceError.swift (100%) create mode 100644 Sources/Confidence/ConfidenceValue.swift create mode 100644 Sources/ConfidenceProvider/Utils/ConfidenceTypeMapper.swift create mode 100644 Tests/ConfidenceProviderTests/ConfidenceTypeMapperTest.swift create mode 100644 Tests/ConfidenceTests/ConfidenceValueTests.swift diff --git a/.gitignore b/.gitignore index ad8ce721..f9658ead 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,4 @@ DerivedData/ .netrc .build .mockingbird -project.json -.swiftpm \ No newline at end of file +project.json \ No newline at end of file diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Confidence-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Confidence-Package.xcscheme new file mode 100644 index 00000000..4f466f8b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Confidence-Package.xcscheme @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Confidence.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Confidence.xcscheme new file mode 100644 index 00000000..0e8ddb01 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Confidence.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ConfidenceProvider.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ConfidenceProvider.xcscheme new file mode 100644 index 00000000..6eaaa02e --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ConfidenceProvider.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index e84a5d2b..e30d2679 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -36,7 +36,7 @@ extension ConfidenceDemoApp { let ctx = MutableContext(targetingKey: UUID.init().uuidString, structure: MutableStructure()) Task { await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: ctx) - confidence.send(eventName: "my_event") + confidence.send(definition: "my_event", payload: ConfidenceStruct()) } } } diff --git a/Package.swift b/Package.swift index 74afe395..545cf790 100644 --- a/Package.swift +++ b/Package.swift @@ -39,6 +39,12 @@ let package = Package( dependencies: [ "ConfidenceProvider", ] - ) + ), + .testTarget( + name: "ConfidenceTests", + dependencies: [ + "Confidence" + ] + ), ] ) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 99d1e282..0fa2f7ab 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -1,7 +1,7 @@ import Foundation public class Confidence: ConfidenceEventSender { - public var context: [String: String] + public var context: ConfidenceStruct public let clientSecret: String public var timeout: TimeInterval public var region: ConfidenceRegion @@ -21,11 +21,11 @@ public class Confidence: ConfidenceEventSender { } // TODO: Implement actual event uploading to the backend - public func send(eventName: String) { - print("Sending \(eventName) - Targeting key: \(context["targeting_key"] ?? "UNKNOWN")") + public func send(definition: String, payload: ConfidenceStruct) { + print("Sending \(definition) - Targeting key: \(payload)") } - public func updateContextEntry(key: String, value: String) { + public func updateContextEntry(key: String, value: ConfidenceValue) { context[key] = value } @@ -38,7 +38,7 @@ public class Confidence: ConfidenceEventSender { } // TODO: Implement creation of child instances - public func withContext(_ context: [String: String]) -> Self { + public func withContext(_ context: ConfidenceStruct) -> Self { return self } } diff --git a/Sources/ConfidenceProvider/ConfidenceError.swift b/Sources/Confidence/ConfidenceError.swift similarity index 100% rename from Sources/ConfidenceProvider/ConfidenceError.swift rename to Sources/Confidence/ConfidenceError.swift diff --git a/Sources/Confidence/ConfidenceEventSender.swift b/Sources/Confidence/ConfidenceEventSender.swift index 4747a543..7fdb49ef 100644 --- a/Sources/Confidence/ConfidenceEventSender.swift +++ b/Sources/Confidence/ConfidenceEventSender.swift @@ -1,7 +1,6 @@ import Foundation /// Sends events to Confidence. Contextual data is appended to each event -// TODO: Add functions for sending events with payload public protocol ConfidenceEventSender: Contextual { - func send(eventName: String) + func send(definition: String, payload: ConfidenceStruct) } diff --git a/Sources/Confidence/ConfidenceValue.swift b/Sources/Confidence/ConfidenceValue.swift new file mode 100644 index 00000000..a6e0e3d0 --- /dev/null +++ b/Sources/Confidence/ConfidenceValue.swift @@ -0,0 +1,272 @@ +import Foundation + +public typealias ConfidenceStruct = [String: ConfidenceValue] + +public class ConfidenceValue: Equatable, Encodable { + private let value: ConfidenceValueInternal + + public init(boolean: Bool) { + self.value = .boolean(boolean) + } + + public init(string: String) { + self.value = .string(string) + } + + public init(integer: Int64) { + self.value = .integer(integer) + } + + public init(double: Double) { + self.value = .double(double) + } + + /// `date` should have at least precision to the "day". + /// If a custom TimeZone is set for the input DateComponents, the internal serializers + /// will convert the input to the local TimeZone before extracting the calendar day. + public init(date: DateComponents) { + self.value = .date(date) + } + + /// If a custom TimeZone is set for the input Date, the internal serializers will convert + /// the input to the local TimeZone (i.e. the local offset information is maintained + /// rather than the one customly set in Date). + public init(timestamp: Date) { + self.value = .timestamp(timestamp) + } + + public init(booleanList: [Bool]) { + self.value = .list(booleanList.map { .boolean($0) }) + } + + public init(stringList: [String]) { + self.value = .list(stringList.map { .string($0) }) + } + + + public init(integerList: [Int64]) { + self.value = .list(integerList.map { .integer($0) }) + } + + public init(doubleList: [Double]) { + self.value = .list(doubleList.map { .double($0) }) + } + + public init(nullList: [()]) { + self.value = .list(nullList.map { .null }) + } + + public init(dateList: [DateComponents]) { + self.value = .list(dateList.map { .date($0) }) + } + + public init(timestampList: [Date]) { + self.value = .list(timestampList.map { .timestamp($0) }) + } + + public init(structure: [String: ConfidenceValue]) { + self.value = .structure(structure.mapValues { $0.value }) + } + + public init(null: ()) { + self.value = .null + } + + private init(valueInternal: ConfidenceValueInternal) { + self.value = valueInternal + } + + public func asBoolean() -> Bool? { + if case let .boolean(bool) = value { + return bool + } + + return nil + } + + public func asString() -> String? { + if case let .string(string) = value { + return string + } + + return nil + } + + public func asInteger() -> Int64? { + if case let .integer(int64) = value { + return int64 + } + + return nil + } + + public func asDouble() -> Double? { + if case let .double(double) = value { + return double + } + + return nil + } + + public func asDateComponents() -> DateComponents? { + if case let .date(dateComponents) = value { + return dateComponents + } + + return nil + } + + public func asDate() -> Date? { + if case let .timestamp(date) = value { + return date + } + + return nil + } + + public func asList() -> [ConfidenceValue]? { + if case let .list(values) = value { + return values.map { i in ConfidenceValue(valueInternal: i) } + } + + return nil + } + + public func asStructure() -> [String: ConfidenceValue]? { + if case let .structure(values) = value { + return values.mapValues { ConfidenceValue(valueInternal: $0) } + } + + return nil + } + + public func isNull() -> Bool { + if case .null = value { + return true + } + + return false + } + + public func type() -> ConfidenceValueType { + switch value { + case .boolean: + return .boolean + case .string: + return .string + case .integer: + return .integer + case .double: + return .double + case .date: + return .date + case .timestamp: + return .timestamp + case .list: + return .list + case .structure: + return .structure + case .null: + return .null + } + } + + public static func == (lhs: ConfidenceValue, rhs: ConfidenceValue) -> Bool { + lhs.value == rhs.value + } +} + +extension ConfidenceValue { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } +} + +public enum ConfidenceValueType: CaseIterable { + case boolean + case string + case integer + case double + case date + case timestamp + case list + case structure + case null +} + + +/// Serializable data structure meant for event sending via Confidence +private enum ConfidenceValueInternal: Equatable, Encodable { + case boolean(Bool) + case string(String) + case integer(Int64) + case double(Double) + case date(DateComponents) + case timestamp(Date) + case list([ConfidenceValueInternal]) + case structure([String: ConfidenceValueInternal]) + case null +} + +extension ConfidenceValueInternal: CustomStringConvertible { + public var description: String { + switch self { + case .boolean(let value): + return "\(value)" + case .string(let value): + return value + case .integer(let value): + return "\(value)" + case .double(let value): + return "\(value)" + case .date(let value): + return "\(value)" + case .timestamp(let value): + return "\(value)" + case .list(value: let values): + return "\(values.map { value in value.description })" + case .structure(value: let values): + return "\(values.mapValues { value in value.description })" + case .null: + return "null" + } + } +} + +extension ConfidenceValueInternal { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch self { + case .null: + try container.encodeNil() + case .integer(let integer): + try container.encode(integer) + case .double(let double): + try container.encode(double) + case .string(let string): + try container.encode(string) + case .boolean(let boolean): + try container.encode(boolean) + case .date(let dateComponents): + let dateFormatter = ISO8601DateFormatter() + dateFormatter.timeZone = TimeZone.current + dateFormatter.formatOptions = [.withFullDate] + if let date = Calendar.current.date(from: dateComponents) { + try container.encode(dateFormatter.string(from: date)) + } else { + throw ConfidenceError.internalError(message: "Could not create date from components") + } + case .timestamp(let date): + let timestampFormatter = ISO8601DateFormatter() + timestampFormatter.timeZone = TimeZone.init(identifier: "UTC") + let timestamp = timestampFormatter.string(from: date) + try container.encode(timestamp) + case .structure(let structure): + try container.encode(structure) + case .list(let list): + try container.encode(list) + } + } +} diff --git a/Sources/Confidence/Contextual.swift b/Sources/Confidence/Contextual.swift index 9b6859ce..390279a6 100644 --- a/Sources/Confidence/Contextual.swift +++ b/Sources/Confidence/Contextual.swift @@ -3,13 +3,12 @@ import Foundation /// A Contextual implementer maintains context data and can create child instances /// that can still access their parent's data public protocol Contextual { - // TODO: Add complex type to the context Dictionary - var context: [String: String] { get set } + var context: ConfidenceStruct { get set } - func updateContextEntry(key: String, value: String) + func updateContextEntry(key: String, value: ConfidenceValue) func removeContextEntry(key: String) func clearContext() /// Creates a child Contextual instance that still has access /// to its parent context - func withContext(_ context: [String: String]) -> Self + func withContext(_ context: ConfidenceStruct) -> Self } diff --git a/Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift b/Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift index 4eb2e182..cf03c2ce 100644 --- a/Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift +++ b/Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature import os diff --git a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift index d4942f73..ddf5ed4d 100644 --- a/Sources/ConfidenceProvider/Cache/DefaultStorage.swift +++ b/Sources/ConfidenceProvider/Cache/DefaultStorage.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence public class DefaultStorage: Storage { private let storageQueue = DispatchQueue(label: "com.confidence.storage") diff --git a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift index 8e703b71..24ca8a48 100644 --- a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift +++ b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift @@ -1,5 +1,6 @@ -import Combine import Foundation +import Combine +import Confidence import OpenFeature import os diff --git a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift index eb272314..ba395350 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature public class LocalStorageResolver: Resolver { diff --git a/Sources/ConfidenceProvider/Utils/ConfidenceTypeMapper.swift b/Sources/ConfidenceProvider/Utils/ConfidenceTypeMapper.swift new file mode 100644 index 00000000..6f1d0edf --- /dev/null +++ b/Sources/ConfidenceProvider/Utils/ConfidenceTypeMapper.swift @@ -0,0 +1,67 @@ +import Foundation +import Confidence +import OpenFeature + +public enum ConfidenceTypeMapper { + static func from(value: Value) -> ConfidenceValue { + return convertValue(value) + } + + static func from(ctx: EvaluationContext) -> ConfidenceStruct { + var ctxMap = ctx.asMap() + ctxMap["targeting_key"] = .string(ctx.getTargetingKey()) + return ctxMap.compactMapValues(convertValue) + } + + // swiftlint:disable:next cyclomatic_complexity + static private func convertValue(_ value: Value) -> ConfidenceValue { + switch value { + case .boolean(let value): + return ConfidenceValue(boolean: value) + case .string(let value): + return ConfidenceValue(string: value) + case .integer(let value): + return ConfidenceValue(integer: value) + case .double(let value): + return ConfidenceValue(double: value) + case .date(let value): + return ConfidenceValue(timestamp: value) + case .list(let values): + let types = Set(values.map(convertValue).map { $0.type() }) + guard types.count == 1, let listType = types.first else { + return ConfidenceValue.init(nullList: [()]) + } + switch listType { + case .boolean: + return ConfidenceValue.init(booleanList: values.compactMap { $0.asBoolean() }) + case .string: + return ConfidenceValue.init(stringList: values.compactMap { $0.asString() }) + case .integer: + return ConfidenceValue.init(integerList: values.compactMap { $0.asInteger() }) + case .double: + return ConfidenceValue.init(doubleList: values.compactMap { $0.asDouble() }) + // Currently Date Value is converted to Timestamp ConfidenceValue to not lose precision, so this should never happen + case .date: + let componentsToExtract: Set = [.year, .month, .day] + return ConfidenceValue.init(dateList: values.compactMap { + guard let date = $0.asDate() else { + return nil + } + return Calendar.current.dateComponents(componentsToExtract, from: date) + }) + case .timestamp: + return ConfidenceValue.init(timestampList: values.compactMap { $0.asDate() }) + case .list: + return ConfidenceValue.init(nullList: values.compactMap { _ in () }) // List of list not allowed + case .structure: + return ConfidenceValue.init(nullList: values.compactMap { _ in () }) // TODO: List of structures + case .null: + return ConfidenceValue.init(nullList: values.compactMap { _ in () }) + } + case .structure(let values): + return ConfidenceValue(structure: values.compactMapValues(convertValue)) + case .null: + return ConfidenceValue(null: ()) + } + } +} diff --git a/Sources/ConfidenceProvider/Utils/HttpStatusCode+Error.swift b/Sources/ConfidenceProvider/Utils/HttpStatusCode+Error.swift index f76c6b8c..7f5ae5a3 100644 --- a/Sources/ConfidenceProvider/Utils/HttpStatusCode+Error.swift +++ b/Sources/ConfidenceProvider/Utils/HttpStatusCode+Error.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature extension HTTPURLResponse { diff --git a/Tests/ConfidenceProviderTests/ConfidenceTypeMapperTest.swift b/Tests/ConfidenceProviderTests/ConfidenceTypeMapperTest.swift new file mode 100644 index 00000000..7fe525c4 --- /dev/null +++ b/Tests/ConfidenceProviderTests/ConfidenceTypeMapperTest.swift @@ -0,0 +1,99 @@ +import Foundation +import Confidence +import OpenFeature +import XCTest + +@testable import ConfidenceProvider + +class ValueConverterTest: XCTestCase { + func testContextConversion() throws { + let openFeatureCtx = MutableContext( + targetingKey: "userid", + structure: MutableStructure(attributes: (["key": .string("value")]))) + let confidenceStruct = ConfidenceTypeMapper.from(ctx: openFeatureCtx) + let expected = [ + "key": ConfidenceValue(string: "value"), + "targeting_key": ConfidenceValue(string: "userid") + ] + XCTAssertEqual(confidenceStruct, expected) + } + + func testContextConversionWithLists() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let date1 = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00")) + let date2 = try XCTUnwrap(formatter.date(from: "2022-01-02 12:00:00")) + + let openFeatureCtx = MutableContext( + targetingKey: "userid", + structure: MutableStructure(attributes: ([ + "stringList": .list([.string("test1"), .string("test2")]), + "booleanList": .list([.boolean(true), .boolean(false)]), + "integerList": .list([.integer(11), .integer(33)]), + "doubleList": .list([.double(3.14), .double(1.0)]), + "dateList": .list([.date(date1), .date(date2)]), + "nullList": .list([.null, .null]), + "listList": .list([.list([.string("nested_value1")]), .list([.string("nested_value2")])]), + "structList": .list([.structure(["test": .string("nested_test1")]), .structure(["test": .string("nested_test2")])]) + ]))) + let confidenceStruct = ConfidenceTypeMapper.from(ctx: openFeatureCtx) + let expected = [ + "stringList": ConfidenceValue(stringList: ["test1", "test2"]), + "booleanList": ConfidenceValue(booleanList: [true, false]), + "integerList": ConfidenceValue(integerList: [11, 33]), + "doubleList": ConfidenceValue(doubleList: [3.14, 1.0]), + "dateList": ConfidenceValue(timestampList: [date1, date2]), + "nullList": ConfidenceValue(nullList: [(), ()]), + "listList": ConfidenceValue(nullList: [(), ()]), + "structList": ConfidenceValue(nullList: [(), ()]), + "targeting_key": ConfidenceValue(string: "userid") + ] + XCTAssertEqual(confidenceStruct, expected) + } + + func testContextConversionWithHeterogenousLists() throws { + let openFeatureCtx = MutableContext( + targetingKey: "userid", + structure: MutableStructure(attributes: (["key": .list([.string("test1"), .integer(1)])]))) + let confidenceStruct = ConfidenceTypeMapper.from(ctx: openFeatureCtx) + let expected = [ + "key": ConfidenceValue(nullList: [()]), + "targeting_key": ConfidenceValue(string: "userid") + ] + XCTAssertEqual(confidenceStruct, expected) + } + + func testValueConversion() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let date = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00")) + + let openFeatureValue = Value.structure([ + "key": .string("value"), + "null": .null, + "bool": .boolean(true), + "int": .integer(3), + "double": .double(4.5), + "date": .date(date), + "list": .list([.integer(3), .integer(5)]), + "structure": .structure(["field1": .string("test"), "field2": .integer(12)]), + ]) + + let confidenceValue = ConfidenceTypeMapper.from(value: openFeatureValue) + let expected = ConfidenceValue(structure: ([ + "key": ConfidenceValue(string: "value"), + "null": ConfidenceValue(null: ()), + "bool": ConfidenceValue(boolean: true), + "int": ConfidenceValue(integer: 3), + "double": ConfidenceValue(double: 4.5), + "date": ConfidenceValue(timestamp: date), + "list": ConfidenceValue(integerList: [3, 5]), + "structure": ConfidenceValue( + structure: [ + "field1": ConfidenceValue(string: "test"), + "field2": ConfidenceValue(integer: 12) + ]) + ])) + XCTAssertEqual(confidenceValue, expected) + } +} diff --git a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift index 330d4a89..566c34dd 100644 --- a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift +++ b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature @testable import ConfidenceProvider diff --git a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift index 5fc4e0b4..beb55a8f 100644 --- a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift +++ b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift @@ -1,4 +1,5 @@ import Foundation +import Confidence import OpenFeature import XCTest diff --git a/Tests/ConfidenceTests/ConfidenceValueTests.swift b/Tests/ConfidenceTests/ConfidenceValueTests.swift new file mode 100644 index 00000000..08617c7d --- /dev/null +++ b/Tests/ConfidenceTests/ConfidenceValueTests.swift @@ -0,0 +1,143 @@ +import Confidence +import XCTest + +final class ConfidenceConfidenceValueTests: XCTestCase { + func testNull() { + let value = ConfidenceValue(null: ()) + XCTAssertTrue(value.isNull()) + } + + func testIntShouldConvertToInt() { + let value = ConfidenceValue(integer: 3) + XCTAssertEqual(value.asInteger(), 3) + } + + func testDoubleShouldConvertToDouble() { + let value = ConfidenceValue(double: 3.14) + XCTAssertEqual(value.asDouble(), 3.14) + } + + func testBoolShouldConvertToBool() { + let value = ConfidenceValue(boolean: true) + XCTAssertEqual(value.asBoolean(), true) + } + + func testStringShouldConvertToString() { + let value = ConfidenceValue(string: "test") + XCTAssertEqual(value.asString(), "test") + } + + func testStringShouldConvertToDate() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let date = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00")) + let value = ConfidenceValue(timestamp: date) + XCTAssertEqual(value.asDate(), date) + } + + func testStringShouldConvertToDateComponents() { + let dateComponents = DateComponents(year: 2024, month: 4, day: 3) + let value = ConfidenceValue(date: dateComponents) + XCTAssertEqual(value.asDateComponents(), dateComponents) + } + + func testListShouldConvertToList() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + formatter.timeZone = TimeZone(abbreviation: "UTC") + let date1 = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00")) + let dateComponents1 = DateComponents(year: 2024, month: 4, day: 3) + let date2 = try XCTUnwrap(formatter.date(from: "2022-01-02 00:00:00")) + let dateComponents2 = DateComponents(year: 2024, month: 4, day: 2) + + let booleanListValue = ConfidenceValue(booleanList: [true, false]) + let integerListValue = ConfidenceValue(integerList: [3, 4]) + let doubleListValue = ConfidenceValue(doubleList: [3.14, 4.0]) + let stringListValue = ConfidenceValue(stringList: ["val1", "val2"]) + let timestampListValue = ConfidenceValue(timestampList: [date1, date2]) + let dateListValue = ConfidenceValue(dateList: [dateComponents1, dateComponents2]) + + XCTAssertEqual(booleanListValue.asList(), [ConfidenceValue(boolean: true), ConfidenceValue(boolean: false)]) + XCTAssertEqual(integerListValue.asList(), [ConfidenceValue(integer: 3), ConfidenceValue(integer: 4)]) + XCTAssertEqual(doubleListValue.asList(), [ConfidenceValue(double: 3.14), ConfidenceValue(double: 4.0)]) + XCTAssertEqual(stringListValue.asList(), [ConfidenceValue(string: "val1"), ConfidenceValue(string: "val2")]) + XCTAssertEqual(timestampListValue.asList(), [ + ConfidenceValue(timestamp: date1), + ConfidenceValue(timestamp: date2) + ]) + XCTAssertEqual(dateListValue.asList(), [ + ConfidenceValue(date: dateComponents1), + ConfidenceValue(date: dateComponents2) + ]) + } + + func testStructShouldConvertToStruct() { + let value = ConfidenceValue(structure: [ + "field1": ConfidenceValue(integer: 3), + "field2": ConfidenceValue(string: "test") + ]) + XCTAssertEqual(value.asStructure(), [ + "field1": ConfidenceValue(integer: 3), + "field2": ConfidenceValue(string: "test") + ]) + } + + func testEmptyListAllowed() { + let value = ConfidenceValue(integerList: []) + XCTAssertEqual(value.asList(), []) + } + + func testWrongTypeDoesntThrow() { + let value = ConfidenceValue(null: ()) + XCTAssertNil(value.asList()) + XCTAssertNil(value.asDouble()) + XCTAssertNil(value.asString()) + XCTAssertNil(value.asBoolean()) + XCTAssertNil(value.asInteger()) + XCTAssertNil(value.asStructure()) + XCTAssertNil(value.asDate()) + XCTAssertNil(value.asDateComponents()) + } + + func testIsNotNull() { + let value = ConfidenceValue(string: "Test") + XCTAssertFalse(value.isNull()) + } + + func testEncodeDecode() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + formatter.timeZone = TimeZone(abbreviation: "EDT") // Verify TimeZone conversion + let date = try XCTUnwrap(formatter.date(from: "2024-04-05 16:00:00")) + let dateComponents = DateComponents(year: 2024, month: 4, day: 3) + + let value = ConfidenceValue(structure: ([ + "bool": ConfidenceValue(boolean: true), + "date": ConfidenceValue(date: dateComponents), + "double": ConfidenceValue(double: 4.5), + "int": ConfidenceValue(integer: 3), + "list": ConfidenceValue(integerList: [3, 5]), + "null": ConfidenceValue(null: ()), + "string": ConfidenceValue(string: "value"), + "structure": ConfidenceValue(structure: ["int": ConfidenceValue(integer: 5)]), + "timestamp": ConfidenceValue(timestamp: date), + ])) + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + let resultString = String(data: try encoder.encode(value), encoding: .utf8) + + let expectedString = """ + {\"bool\":true, + \"date\":\"2024-04-03\", + \"double\":4.5, + \"int\":3, + \"list\":[3,5], + \"null\":null, + \"string\":\"value\", + \"structure\":{\"int\":5}, + \"timestamp\":\"2024-04-05T20:00:00Z"} + """.replacingOccurrences(of: "\n", with: "") // Newlines were added for readability + + XCTAssertEqual(resultString, expectedString) + } +}