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)
+ }
+}