From 01db167214badcbb795be4cb29f6ac656fd64769 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 17 Mar 2024 01:46:18 +0100 Subject: [PATCH] Optimise the Swift version of Vexilla Client (#42) --- .github/workflows/test.yml | 1 - clients/swift/Package.swift => Package.swift | 6 +- clients/swift/.vscode/extensions.json | 5 +- .../swift/Sources/VexillaClient/Hashing.swift | 7 +- .../Sources/VexillaClient/Scheduling.swift | 6 +- .../swift/Sources/VexillaClient/Types.swift | 637 +++++------------- .../swift/Sources/VexillaClient/Utils.swift | 11 - .../Sources/VexillaClient/VexillaClient.swift | 246 +++---- .../VexillaClientTests/HashingTests.swift | 8 +- .../VexillaClientTests.swift | 128 +--- 10 files changed, 314 insertions(+), 741 deletions(-) rename clients/swift/Package.swift => Package.swift (83%) delete mode 100644 clients/swift/Sources/VexillaClient/Utils.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3909522..deecbf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -229,7 +229,6 @@ jobs: steps: - uses: actions/checkout@v3 - run: | - cd ./clients/swift swift build swift test env: diff --git a/clients/swift/Package.swift b/Package.swift similarity index 83% rename from clients/swift/Package.swift rename to Package.swift index e01b73c..f10194e 100644 --- a/clients/swift/Package.swift +++ b/Package.swift @@ -21,11 +21,13 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "VexillaClient", - dependencies: [] + dependencies: [], + path: "clients/swift/Sources/VexillaClient" ), .testTarget( name: "VexillaClientTests", - dependencies: ["VexillaClient"] + dependencies: ["VexillaClient"], + path: "clients/swift/Tests/VexillaClientTests" ), ] ) diff --git a/clients/swift/.vscode/extensions.json b/clients/swift/.vscode/extensions.json index 2aa0c92..b10a4f0 100644 --- a/clients/swift/.vscode/extensions.json +++ b/clients/swift/.vscode/extensions.json @@ -1,8 +1,5 @@ { "recommendations": [ - "DEVSENSE.composer-php-vscode", - "DEVSENSE.intelli-php-vscode", - "DEVSENSE.phptools-vscode", - "DEVSENSE.profiler-php-vscode" + "sswg.swift-lang" ] } diff --git a/clients/swift/Sources/VexillaClient/Hashing.swift b/clients/swift/Sources/VexillaClient/Hashing.swift index dceb498..867fac7 100644 --- a/clients/swift/Sources/VexillaClient/Hashing.swift +++ b/clients/swift/Sources/VexillaClient/Hashing.swift @@ -1,10 +1,9 @@ import Foundation func hashString(stringToHash: String, seed: Float64) -> Float64 { - let chars = Array(stringToHash) - - let total = chars.reduce(0) { a, b in - a + Int(b.asciiValue ?? 0) + var total = 0 + for char in stringToHash.utf8 { + total += Int(char) } var calculated = Float64(total) * seed * 42.0 diff --git a/clients/swift/Sources/VexillaClient/Scheduling.swift b/clients/swift/Sources/VexillaClient/Scheduling.swift index 56e3db8..f3e513a 100644 --- a/clients/swift/Sources/VexillaClient/Scheduling.swift +++ b/clients/swift/Sources/VexillaClient/Scheduling.swift @@ -12,7 +12,7 @@ func isScheduleActiveWithNow(schedule: Schedule, scheduleType: ScheduleType, now let nowSeconds = now.timeIntervalSince1970 var calendar = Calendar(identifier: Calendar.Identifier.iso8601) guard let utc = TimeZone(identifier: "UTC") else { - throw "Could not create UTC calendar" + throw VexillaSchedulingError.couldNotCreateUTCTimezone } calendar.timeZone = utc @@ -26,11 +26,11 @@ func isScheduleActiveWithNow(schedule: Schedule, scheduleType: ScheduleType, now let endDate = Date(timeIntervalSince1970: TimeInterval(schedule.end / 1000)) guard let dayAfterEndDate = calendar.date(byAdding: .day, value: 1, to: endDate) else { - throw "could not add day to endDate" + throw VexillaSchedulingError.couldNotAddDayToEndDate } let endOfEndDate = calendar.startOfDay(for: dayAfterEndDate) - if startOfStartDate > now || endOfEndDate < now { + guard startOfStartDate <= now && endOfEndDate >= now else { return false } diff --git a/clients/swift/Sources/VexillaClient/Types.swift b/clients/swift/Sources/VexillaClient/Types.swift index dbd5aa3..5804e55 100644 --- a/clients/swift/Sources/VexillaClient/Types.swift +++ b/clients/swift/Sources/VexillaClient/Types.swift @@ -1,14 +1,23 @@ import Foundation -// TODO: Make a proper Error enum for Scheduling +// TODO: Make a proper Error enum for VexillaClient extension String: Error {} +enum VexillaSchedulingError: Error { + case couldNotCreateUTCTimezone + case couldNotAddDayToEndDate +} + +enum VexillaDecodingError: Error { + case couldNotDecodeRawValue(expectedType: Any.Type) +} + extension RawRepresentable { init(rawValue: RawValue) throws { if let result = Self(rawValue: rawValue) { self = result } else { - throw "Could not parse rawValue (\(rawValue)) into \(Self.self)" + throw VexillaDecodingError.couldNotDecodeRawValue(expectedType: Self.self) } } } @@ -51,28 +60,10 @@ public struct Schedule: Decodable { let endTime: Int } -extension Schedule { - init(fromDict: [String: Any]) throws { - start = try safeGet(dict: fromDict, key: "start") - end = try safeGet(dict: fromDict, key: "end") - timezone = try safeGet(dict: fromDict, key: "timezone") - let _timeType: String = try safeGet(dict: fromDict, key: "timeType") - timeType = try ScheduleTimeType(rawValue: _timeType) - startTime = try safeGet(dict: fromDict, key: "startTime") - endTime = try safeGet(dict: fromDict, key: "endTime") - } -} - public struct GroupMeta: Decodable { let version: String } -extension GroupMeta { - init(fromDict: [String: Any]) throws { - version = try safeGet(dict: fromDict, key: "version") - } -} - protocol BaseFeature: Decodable { var name: String { get } var featureId: String { get } @@ -81,350 +72,205 @@ protocol BaseFeature: Decodable { var schedule: Schedule { get } } -public struct Feature: BaseFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType - public let scheduleType: ScheduleType - public let schedule: Schedule - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) +public enum Feature: Decodable, BaseFeature { + private enum CodingKeys: String, CodingKey { + case featureType } -} - -public struct ToggleFeature: BaseFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .toggle - public let scheduleType: ScheduleType - public let schedule: Schedule - public let value: Bool - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - value = try safeGet(dict: fromDict, key: "value") + case toggle(ToggleFeature) + case gradual(GradualFeature) + case selective(SelectiveFeature) + case value(ValueFeature) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let featureType = try container.decode(FeatureType.self, forKey: .featureType) + + switch featureType { + case .toggle: + self = try .toggle(ToggleFeature(from: decoder)) + case .gradual: + self = try .gradual(GradualFeature(from: decoder)) + case .selective: + self = try .selective(SelectiveFeature(from: decoder)) + case .value: + self = try .value(ValueFeature(from: decoder)) + } } -} -public struct GradualFeature: BaseFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .gradual - public let scheduleType: ScheduleType - public let schedule: Schedule - public let value: Float64 - public let seed: Float64 - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - value = try safeGet(dict: fromDict, key: "value") - seed = try safeGet(dict: fromDict, key: "seed") + // MARK: Helpful accessors for both internal and public use + + public var name: String { + switch self { + case let .toggle(feature): + return feature.name + case let .gradual(feature): + return feature.name + case let .selective(feature): + return feature.name + case let .value(feature): + return feature.name + } } -} -protocol BaseSelectiveFeature: BaseFeature { - var valueType: ValueType { get } -} + public var featureId: String { + switch self { + case let .toggle(feature): + return feature.featureId + case let .gradual(feature): + return feature.featureId + case let .selective(feature): + return feature.featureId + case let .value(feature): + return feature.featureId + } + } -public struct SelectiveFeature: BaseSelectiveFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public let valueType: ValueType - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) + public var featureType: FeatureType { + switch self { + case .toggle: + return .toggle + case .gradual: + return .gradual + case .selective: + return .selective + case .value: + return .value + } } -} -public struct SelectiveStringFeature: BaseSelectiveFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public var valueType: ValueType = .string - public let value: [String] - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - value = try safeGet(dict: fromDict, key: "value") + public var scheduleType: ScheduleType { + switch self { + case let .toggle(feature): + return feature.scheduleType + case let .gradual(feature): + return feature.scheduleType + case let .selective(feature): + return feature.scheduleType + case let .value(feature): + return feature.scheduleType + } } -} -protocol BaseSelectiveNumberFeature: BaseSelectiveFeature { - var numberType: NumberType { get } + public var schedule: Schedule { + switch self { + case let .toggle(feature): + return feature.schedule + case let .gradual(feature): + return feature.schedule + case let .selective(feature): + return feature.schedule + case let .value(feature): + return feature.schedule + } + } } -public struct SelectiveNumberFeature: BaseSelectiveNumberFeature { +public struct ToggleFeature: BaseFeature { public let name: String public let featureId: String - public var featureType: FeatureType = .selective + public var featureType: FeatureType { .toggle } public let scheduleType: ScheduleType public let schedule: Schedule - public var valueType: ValueType = .number - public let numberType: NumberType - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) - } + public let value: Bool } -public struct SelectiveIntFeature: BaseSelectiveNumberFeature { +public struct GradualFeature: BaseFeature { public let name: String public let featureId: String - public var featureType: FeatureType = .selective + public var featureType: FeatureType { .gradual } public let scheduleType: ScheduleType public let schedule: Schedule - public var valueType: ValueType = .number - public var numberType: NumberType = .int - public let value: [Int64] - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) - value = try safeGet(dict: fromDict, key: "value") - } + public let value: Float64 + public let seed: Float64 } -public struct SelectiveFloatFeature: BaseSelectiveNumberFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public var valueType: ValueType = .number - public var numberType: NumberType = .float - public let value: [Float64] - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) - value = try safeGet(dict: fromDict, key: "value") +public struct SelectiveFeature: BaseFeature { + private enum CodingKeys: String, CodingKey { + case name, featureId, scheduleType, schedule, valueType, numberType, value } -} -protocol BaseValueFeature: BaseFeature { - var valueType: ValueType { get } -} + public enum Value { + case string([String]) + case int([Int64]) + case float([Float64]) + } -public struct ValueFeature: BaseValueFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public let valueType: ValueType - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + featureId = try container.decode(String.self, forKey: .featureId) + scheduleType = try container.decode(ScheduleType.self, forKey: .scheduleType) + schedule = try container.decode(Schedule.self, forKey: .schedule) + let valueType = try container.decode(ValueType.self, forKey: .valueType) + + switch valueType { + case .string: + let value = try container.decode([String].self, forKey: .value) + self.value = .string(value) + case .number: + let numberType = try container.decode(NumberType.self, forKey: .numberType) + switch numberType { + case .int: + let value = try container.decode([Int64].self, forKey: .value) + self.value = .int(value) + case .float: + let value = try container.decode([Float64].self, forKey: .value) + self.value = .float(value) + } + } } -} -public struct ValueStringFeature: BaseValueFeature { public let name: String public let featureId: String - public var featureType: FeatureType = .selective + public var featureType: FeatureType { .selective } public let scheduleType: ScheduleType public let schedule: Schedule - public var valueType: ValueType = .string - public let value: String - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - value = try safeGet(dict: fromDict, key: "value") - } + public let value: Value } -protocol BaseValueNumberFeature: BaseValueFeature { - var numberType: NumberType { get } -} +public struct ValueFeature: BaseFeature { + private enum CodingKeys: String, CodingKey { + case name, featureId, scheduleType, schedule, valueType, numberType, value + } -public struct ValueNumberFeature: BaseValueNumberFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public var valueType: ValueType = .number - public let numberType: NumberType - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) + public enum Value { + case string(String) + case int(Int64) + case float(Float64) } -} -public struct ValueIntFeature: BaseValueNumberFeature { - public let name: String - public let featureId: String - public var featureType: FeatureType = .selective - public let scheduleType: ScheduleType - public let schedule: Schedule - public var valueType: ValueType = .number - public var numberType: NumberType = .int - public let value: Int64 - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) - value = try safeGet(dict: fromDict, key: "value") + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + featureId = try container.decode(String.self, forKey: .featureId) + scheduleType = try container.decode(ScheduleType.self, forKey: .scheduleType) + schedule = try container.decode(Schedule.self, forKey: .schedule) + let valueType = try container.decode(ValueType.self, forKey: .valueType) + + switch valueType { + case .string: + let value = try container.decode(String.self, forKey: .value) + self.value = .string(value) + case .number: + let numberType = try container.decode(NumberType.self, forKey: .numberType) + switch numberType { + case .int: + let value = try container.decode(Int64.self, forKey: .value) + self.value = .int(value) + case .float: + let value = try container.decode(Float64.self, forKey: .value) + self.value = .float(value) + } + } } -} -public struct ValueFloatFeature: BaseValueNumberFeature { public let name: String public let featureId: String - public var featureType: FeatureType = .selective + public var featureType: FeatureType { .value } public let scheduleType: ScheduleType public let schedule: Schedule - public var valueType: ValueType = .number - public var numberType: NumberType = .float - public let value: Float64 - - init(fromDict: [String: Any]) throws { - name = try safeGet(dict: fromDict, key: "name") - featureId = try safeGet(dict: fromDict, key: "featureId") - let _featureType: String = try safeGet(dict: fromDict, key: "featureType") - featureType = try FeatureType(rawValue: _featureType) - let _scheduleType: String = try safeGet(dict: fromDict, key: "scheduleType") - scheduleType = try ScheduleType(rawValue: _scheduleType) - let rawSchedule: [String: Any] = try safeGet(dict: fromDict, key: "schedule") - schedule = try Schedule(fromDict: rawSchedule) - - let _valueType: String = try safeGet(dict: fromDict, key: "valueType") - valueType = try ValueType(rawValue: _valueType) - - let _numberType: String = try safeGet(dict: fromDict, key: "numberType") - numberType = try NumberType(rawValue: _numberType) - value = try safeGet(dict: fromDict, key: "value") - } + public let value: Value } public struct ManifestGroup: Decodable { @@ -437,163 +283,16 @@ public struct Manifest: Decodable { public let groups: [ManifestGroup] } -public struct Environment { +public struct Environment: Decodable { public let name: String public let environmentId: String - public let rawFeatures: [String: Feature] - - let toggleFeatures: [String: ToggleFeature] - let gradualFeatures: [String: GradualFeature] - - let selectiveFeatures: [String: SelectiveFeature] - let selectiveStringFeatures: [String: SelectiveStringFeature] - let selectiveNumberFeatures: [String: SelectiveNumberFeature] - let selectiveIntFeatures: [String: SelectiveIntFeature] - let selectiveFloatFeatures: [String: SelectiveFloatFeature] - - let valueFeatures: [String: ValueFeature] - let valueStringFeatures: [String: ValueStringFeature] - let valueNumberFeatures: [String: ValueNumberFeature] - let valueIntFeatures: [String: ValueIntFeature] - let valueFloatFeatures: [String: ValueFloatFeature] - - init(fromDict: [String: Any]) throws { - // let data = try JSONSerialization.jsonObject(with: Data(json.utf8), options: .allowFragments) as? [String: Any] - - name = try safeGet(dict: fromDict, key: "name") - environmentId = try safeGet(dict: fromDict, key: "environmentId") - - let _features: [String: [String: Any]] = try safeGet(dict: fromDict, key: "features") - - var _rawFeatures: [String: Feature] = [:] - - var _toggleFeatures: [String: ToggleFeature] = [:] - var _gradualFeatures: [String: GradualFeature] = [:] - - var _selectiveFeatures: [String: SelectiveFeature] = [:] - var _selectiveStringFeatures: [String: SelectiveStringFeature] = [:] - var _selectiveNumberFeatures: [String: SelectiveNumberFeature] = [:] - var _selectiveIntFeatures: [String: SelectiveIntFeature] = [:] - var _selectiveFloatFeatures: [String: SelectiveFloatFeature] = [:] - - var _valueFeatures: [String: ValueFeature] = [:] - var _valueStringFeatures: [String: ValueStringFeature] = [:] - var _valueNumberFeatures: [String: ValueNumberFeature] = [:] - var _valueIntFeatures: [String: ValueIntFeature] = [:] - var _valueFloatFeatures: [String: ValueFloatFeature] = [:] - - for (key, value) in _features { - let rawFeature = try Feature(fromDict: value) - _rawFeatures[key] = rawFeature - - switch rawFeature.featureType { - case .toggle: - let toggleFeature = try ToggleFeature(fromDict: value) - _toggleFeatures[key] = toggleFeature - case .gradual: - let gradualFeature = try GradualFeature(fromDict: value) - _gradualFeatures[key] = gradualFeature - case .selective: - let selectiveFeature = try SelectiveFeature(fromDict: value) - _selectiveFeatures[key] = selectiveFeature - - switch selectiveFeature.valueType { - case .string: - let selectiveStringFeature = try SelectiveStringFeature(fromDict: value) - _selectiveStringFeatures[key] = selectiveStringFeature - - case .number: - let selectiveNumberFeature = try SelectiveNumberFeature(fromDict: value) - _selectiveNumberFeatures[key] = selectiveNumberFeature - - switch selectiveNumberFeature.numberType { - case .int: - let selectiveIntFeature = try SelectiveIntFeature(fromDict: value) - _selectiveIntFeatures[key] = selectiveIntFeature - - case .float: - let selectiveFloatFeature = try SelectiveFloatFeature(fromDict: value) - _selectiveFloatFeatures[key] = selectiveFloatFeature - } - } - - case .value: - let valueFeature = try ValueFeature(fromDict: value) - _valueFeatures[key] = valueFeature - - switch valueFeature.valueType { - case .string: - let valueStringFeature = try ValueStringFeature(fromDict: value) - _valueStringFeatures[key] = valueStringFeature - - case .number: - let valueNumberFeature = try ValueNumberFeature(fromDict: value) - _valueNumberFeatures[key] = valueNumberFeature - - switch valueNumberFeature.numberType { - case .int: - let valueIntFeature = try ValueIntFeature(fromDict: value) - _valueIntFeatures[key] = valueIntFeature - - case .float: - let valueFloatFeature = try ValueFloatFeature(fromDict: value) - _valueFloatFeatures[key] = valueFloatFeature - } - } - } - } - - rawFeatures = _rawFeatures - - toggleFeatures = _toggleFeatures - gradualFeatures = _gradualFeatures - - selectiveFeatures = _selectiveFeatures - selectiveStringFeatures = _selectiveStringFeatures - selectiveNumberFeatures = _selectiveNumberFeatures - selectiveIntFeatures = _selectiveIntFeatures - selectiveFloatFeatures = _selectiveFloatFeatures - - valueFeatures = _valueFeatures - valueStringFeatures = _valueStringFeatures - valueNumberFeatures = _valueNumberFeatures - valueIntFeatures = _valueIntFeatures - valueFloatFeatures = _valueFloatFeatures - } + public let features: [String: Feature] } -public struct Group { +public struct Group: Decodable { public let name: String public let groupId: String public let meta: GroupMeta public let environments: [String: Environment] public let features: [String: Feature] - - init(json: String) throws { - // Maybe have a separate assertion here if the jsonObject function fails - let data = try JSONSerialization.jsonObject(with: Data(json.utf8), options: .allowFragments) as? [String: Any] - - name = try safeGet(dict: data, key: "name") - groupId = try safeGet(dict: data, key: "groupId") - - let _groupMeta: [String: Any] = try safeGet(dict: data, key: "meta") - let metaVersion: String = try safeGet(dict: _groupMeta, key: "version") - meta = GroupMeta(version: metaVersion) - - var _environments: [String: Environment] = [:] - let _rawEnvironments: [String: [String: Any]] = try safeGet(dict: data, key: "environments") - for (key, value) in _rawEnvironments { - _environments[key] = try Environment(fromDict: value) - } - - environments = _environments - - var _features: [String: Feature] = [:] - let _rawFeatures: [String: [String: Any]] = try safeGet(dict: data, key: "features") - for (key, value) in _rawFeatures { - _features[key] = try Feature(fromDict: value) - } - - features = _features - } } diff --git a/clients/swift/Sources/VexillaClient/Utils.swift b/clients/swift/Sources/VexillaClient/Utils.swift deleted file mode 100644 index 9364faa..0000000 --- a/clients/swift/Sources/VexillaClient/Utils.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -func safeGet(dict: [String: Any]?, key: String, errorMessage: String? = nil) throws -> T { - let actualErrorMessage = errorMessage ?? "Could not safely get value for key (\(key)) from dict" - - if let value = dict?[key] as? T { - return value - } else { - throw actualErrorMessage - } -} diff --git a/clients/swift/Sources/VexillaClient/VexillaClient.swift b/clients/swift/Sources/VexillaClient/VexillaClient.swift index 62baf9e..f25f50e 100644 --- a/clients/swift/Sources/VexillaClient/VexillaClient.swift +++ b/clients/swift/Sources/VexillaClient/VexillaClient.swift @@ -19,14 +19,13 @@ public struct VexillaClient { self.instanceId = instanceId } - public func getManifest(fetch: (String) async throws -> String) async throws -> Manifest { - let url = "\(baseUrl)/manifest.json" + public func getManifest(fetch: (URL) async throws -> Data) async throws -> Manifest { + guard let url = URL(string: "\(baseUrl)/manifest.json") else { + throw "Internal VexillaClient Error: Invalid URL" + } let response = try await fetch(url) - // print("getManifest Response", response.utf8) - let responseData = Data(response.utf8) - let manifest = try JSONDecoder().decode(Manifest.self, from: responseData) - return manifest + return try JSONDecoder().decode(Manifest.self, from: response) } public mutating func setManifest(manifest: Manifest) { @@ -41,28 +40,27 @@ public struct VexillaClient { groupLookupTable = _groupLookupTable } - public mutating func syncManifest(fetch: (String) async throws -> String) async throws { + public mutating func syncManifest(fetch: (URL) async throws -> Data) async throws { let manifest = try await getManifest(fetch: fetch) setManifest(manifest: manifest) } - public func getFlags(groupNameOrId: String, fetch: (String) async throws -> String) async throws -> Group { - let groupId: String = try safeGet(dict: groupLookupTable, key: groupNameOrId, errorMessage: "Group ID (\(groupNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?") + public func getFlags(groupNameOrId: String, fetch: (URL) async throws -> Data) async throws -> Group { + guard let groupId = groupLookupTable[groupNameOrId] else { + throw "Group ID (\(groupNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } let url = "\(baseUrl)/\(groupId).json" - let response = try await fetch(url) - // let responseData = Data(response.utf8) - do { - // let group = try JSONDecoder().decode(Group.self, from: responseData) - let group = try Group(json: response) - return group - } catch { - print("GROUP ERROR", error) - throw error + guard let url = URL(string: url) else { + throw "Internal VexillaClient Error: Invalid URL" } + let response = try await fetch(url) + return try JSONDecoder().decode(Group.self, from: response) } public mutating func setFlags(groupNameOrId: String, group: Group) throws { - let groupId: String = try safeGet(dict: groupLookupTable, key: groupNameOrId, errorMessage: "Group ID (\(groupNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?") + guard let groupId = groupLookupTable[groupNameOrId] else { + throw "Group ID (\(groupNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } flagGroups[groupId] = group @@ -83,7 +81,7 @@ public struct VexillaClient { } } - public mutating func syncFlags(groupNameOrId: String, fetch: (String) async throws -> String) async throws { + public mutating func syncFlags(groupNameOrId: String, fetch: (URL) async throws -> Data) async throws { let group = try await getFlags(groupNameOrId: groupNameOrId, fetch: fetch) try setFlags(groupNameOrId: groupNameOrId, group: group) } @@ -93,31 +91,26 @@ public struct VexillaClient { } public func shouldCustomString(groupNameOrId: String, featureNameOrId: String, instanceId: String) throws -> Bool { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return false } - switch feature.featureType { - case .toggle: - let toggleFeature: ToggleFeature = try safeGet(dict: environment.toggleFeatures, key: feature.featureId) + switch feature { + case .toggle(let toggleFeature): return toggleFeature.value - case .gradual: - let gradualFeature: GradualFeature = try safeGet(dict: environment.gradualFeatures, key: feature.featureId) + case .gradual(let gradualFeature): return hashString(stringToHash: instanceId, seed: gradualFeature.seed) <= gradualFeature.value - case .selective: - let selectiveFeature: SelectiveFeature = try safeGet(dict: environment.selectiveFeatures, key: feature.featureId) - - switch selectiveFeature.valueType { - case .string: - let selectiveStringFeature: SelectiveStringFeature = try safeGet(dict: environment.selectiveStringFeatures, key: feature.featureId) - return selectiveStringFeature.value.contains(instanceId) + case .selective(let selectiveFeature): + switch selectiveFeature.value { + case .string(let values): + return values.contains(instanceId) default: - throw "should function must only be called for features with a valueType of 'string'. Try shouldCustomInt, shouldCustomInt64, shouldCustomFloat, or shouldCustomFloat64" + throw "\(#function) must only be called for features with a valueType of 'number'. Try should or shouldCustomString" } case .value: - throw "should cannot be called on features with featureType of 'value'" + throw "\(#function) should cannot be called on features with featureType of 'value'" } } @@ -126,37 +119,26 @@ public struct VexillaClient { } public func shouldCustomInt64(groupNameOrId: String, featureNameOrId: String, instanceId: Int64) throws -> Bool { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return false } - switch feature.featureType { - case .toggle: - let toggleFeature: ToggleFeature = try safeGet(dict: environment.toggleFeatures, key: feature.featureId) + switch feature { + case .toggle(let toggleFeature): return toggleFeature.value - case .gradual: - let gradualFeature: GradualFeature = try safeGet(dict: environment.gradualFeatures, key: feature.featureId) + case .gradual(let gradualFeature): return hashInt64(intToHash: instanceId, seed: gradualFeature.seed) > gradualFeature.value - case .selective: - let selectiveFeature: SelectiveFeature = try safeGet(dict: environment.selectiveFeatures, key: feature.featureId) - - switch selectiveFeature.valueType { - case .number: - let selectiveNumberFeature: SelectiveNumberFeature = try safeGet(dict: environment.selectiveNumberFeatures, key: feature.featureId) - switch selectiveNumberFeature.numberType { - case .int: - let selectiveIntFeature: SelectiveIntFeature = try safeGet(dict: environment.selectiveIntFeatures, key: feature.featureId) - return selectiveIntFeature.value.contains(instanceId) - default: - throw "shouldCustomInt/shouldCustomInt64 function must only be called for features with a numberType of 'int'. Try shouldFloat or shouldFloat64" - } + case .selective(let selectiveFeature): + switch selectiveFeature.value { + case .int(let values): + return values.contains(instanceId) default: - throw "shouldCustomInt/shouldCustomInt64 function must only be called for features with a valueType of 'number'. Try should or shouldCustomString" + throw "\(#function) must only be called for features with an int value. Try should or shouldCustomString" } case .value: - throw "should cannot be called on features with featureType of 'value'" + throw "\(#function) should cannot be called on features with featureType of 'value'" } } @@ -165,88 +147,67 @@ public struct VexillaClient { } public func shouldCustomFloat64(groupNameOrId: String, featureNameOrId: String, instanceId: Float64) throws -> Bool { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return false } - switch feature.featureType { - case .toggle: - let toggleFeature: ToggleFeature = try safeGet(dict: environment.toggleFeatures, key: feature.featureId) + switch feature { + case .toggle(let toggleFeature): return toggleFeature.value - case .gradual: - let gradualFeature: GradualFeature = try safeGet(dict: environment.gradualFeatures, key: feature.featureId) + case .gradual(let gradualFeature): return hashFloat64(floatToHash: instanceId, seed: gradualFeature.seed) > gradualFeature.value - case .selective: - let selectiveFeature: SelectiveFeature = try safeGet(dict: environment.selectiveFeatures, key: feature.featureId) - - switch selectiveFeature.valueType { - case .number: - let selectiveNumberFeature: SelectiveNumberFeature = try safeGet(dict: environment.selectiveNumberFeatures, key: feature.featureId) - switch selectiveNumberFeature.numberType { - case .float: - let selectiveFloatFeature: SelectiveFloatFeature = try safeGet(dict: environment.selectiveFloatFeatures, key: feature.featureId) - return selectiveFloatFeature.value.contains(instanceId) - default: - throw "shouldCustomFloat/shouldCustomFloat64 function must only be called for features with a numberType of 'float'. Try shouldInt or shouldInt64" - } + case .selective(let selectiveFeature): + switch selectiveFeature.value { + case .float(let values): + return values.contains(instanceId) default: - throw "shouldCustomFloat/shouldCustomFloat64 function must only be called for features with a valueType of 'number'. Try should or shouldCustomString" + throw "\(#function) must only be called for features with a valueType of 'number'. Try should or shouldCustomString" } case .value: - throw "should cannot be called on features with featureType of 'value'" + throw "\(#function) should cannot be called on features with featureType of 'value'" } } public func valueString(groupNameOrId: String, featureNameOrId: String, defaultString: String) throws -> String { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return defaultString } - switch feature.featureType { - case .value: - let valueFeature: ValueFeature = try safeGet(dict: environment.valueFeatures, key: feature.featureId) + guard case .value(let valueFeature) = feature else { + throw "\(#function) can only be called on features with featureType of 'value'" + } - switch valueFeature.valueType { - case .string: - let valueStringFeature: ValueStringFeature = try safeGet(dict: environment.valueStringFeatures, key: feature.featureId) - return valueStringFeature.value - default: - throw "valueString function must only be called for features with a valueType of 'string'. Try valueInt, valueInt64, valueFloat, or valueFloat64" - } - default: - throw "valueString can only be called on features with featureType of 'value'" + guard case .string(let string) = valueFeature.value else { + throw "\(#function) can only be called on features with a valueType of 'string'" } + + return string } - public func valueInt(groupNameOrId: String, featureNameOrId: String, defaultInt32: Int32) throws -> Int32 { + public func valueInt32(groupNameOrId: String, featureNameOrId: String, defaultInt32: Int32) throws -> Int32 { return try Int32(valueInt64(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId, defaultInt64: Int64(defaultInt32))) } public func valueInt64(groupNameOrId: String, featureNameOrId: String, defaultInt64: Int64) throws -> Int64 { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return defaultInt64 } - switch feature.featureType { - case .value: - let valueFeature: ValueFeature = try safeGet(dict: environment.valueFeatures, key: feature.featureId) - - switch valueFeature.valueType { - case .number: - let valueIntFeature: ValueIntFeature = try safeGet(dict: environment.valueIntFeatures, key: feature.featureId) - return valueIntFeature.value - default: - throw "valueInt/valueInt64 functions must only be called for features with a valueType of 'int'. Try valueString, valueFloat, or valueFloat64" - } - default: - throw "valueString can only be called on features with featureType of 'value'" + guard case .value(let valueFeature) = feature else { + throw "\(#function) can only be called on features with featureType of 'value'" + } + + guard case .int(let int) = valueFeature.value else { + throw "\(#function) can only be called on features with a valueType of 'int'" } + + return int } public func valueFloat(groupNameOrId: String, featureNameOrId: String, defaultFloat32: Float32) throws -> Float32 { @@ -254,45 +215,56 @@ public struct VexillaClient { } public func valueFloat64(groupNameOrId: String, featureNameOrId: String, defaultFloat64: Float64) throws -> Float64 { - let (environment, feature) = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) + let feature = try getFeature(groupNameOrId: groupNameOrId, featureNameOrId: featureNameOrId) - if try !isScheduledFeatureActive(feature: feature) { + guard try isScheduledFeatureActive(feature: feature) else { return defaultFloat64 } - switch feature.featureType { - case .value: - let valueFeature: ValueFeature = try safeGet(dict: environment.valueFeatures, key: feature.featureId) + guard case .value(let valueFeature) = feature else { + throw "\(#function) can only be called on features with featureType of 'value'" + } - switch valueFeature.valueType { - case .number: - let valueFloatFeature: ValueFloatFeature = try safeGet(dict: environment.valueFloatFeatures, key: feature.featureId) - return valueFloatFeature.value - default: - throw "valueFloat/valueFloat64 functions must only be called for features with a valueType of 'float'. Try valueString, valueInt, or valueInt64" - } - default: - throw "valueString can only be called on features with featureType of 'value'" + guard case .float(let float) = valueFeature.value else { + throw "\(#function) can only be called on features with a valueType of 'float'" } - } - private func getFeature(groupNameOrId: String, featureNameOrId: String) throws -> (Environment, Feature) { - // print("GETTING FEATURE:", groupNameOrId) - // dump(groupLookupTable) - let groupId: String = try safeGet(dict: groupLookupTable, key: groupNameOrId) + return float + } - let groupEnvironmentLookupTable: [String: String] = try safeGet(dict: environmentLookupTable, key: groupId) - let environmentId: String = try safeGet(dict: groupEnvironmentLookupTable, key: environment) + private func getFeature(groupNameOrId: String, featureNameOrId: String) throws -> Feature { + guard let groupId = groupLookupTable[groupNameOrId] else { + throw "Group ID (\(groupNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } + + guard let groupEnvironmentLookupTable = environmentLookupTable[groupId] else { + throw "Environment lookup table not found for group ID (\(groupId)). Did you fetch and set the manifest, yet?" + } + + guard let environmentId = groupEnvironmentLookupTable[environment] else { + throw "Environment (\(environment)) not found in lookup table. Did you fetch and set the manifest, yet?" + } - let groupFeatureLookupTable: [String: String] = try safeGet(dict: featureLookupTable, key: groupId) - let featureId: String = try safeGet(dict: groupFeatureLookupTable, key: featureNameOrId) + guard let featureLookupTable = featureLookupTable[groupId] else { + throw "Feature lookup table not found for group ID (\(groupId)). Did you fetch and set the manifest, yet?" + } - let group: Group = try safeGet(dict: flagGroups, key: groupId) + guard let featureId = featureLookupTable[featureNameOrId] else { + throw "Feature ID (\(featureNameOrId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } - let environment: Environment = try safeGet(dict: group.environments, key: environmentId) + guard let group = flagGroups[groupId] else { + throw "Group ID (\(groupId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } - let feature: Feature = try safeGet(dict: environment.rawFeatures, key: featureId) + guard let environment = group.environments[environmentId] else { + throw "Environment (\(environment)) not found in lookup table. Did you fetch and set the manifest, yet?" + } - return (environment, feature) + guard let feature = environment.features[featureId] else { + throw "Feature ID (\(featureId)) not found in lookup table. Did you fetch and set the manifest, yet?" + } + + return feature } } diff --git a/clients/swift/Tests/VexillaClientTests/HashingTests.swift b/clients/swift/Tests/VexillaClientTests/HashingTests.swift index eca0e88..00b0004 100644 --- a/clients/swift/Tests/VexillaClientTests/HashingTests.swift +++ b/clients/swift/Tests/VexillaClientTests/HashingTests.swift @@ -8,18 +8,18 @@ final class VexillaHashingTests: XCTestCase { private let seedB = 0.22 func testStringWorking() { - XCTAssertEqual(hashString(stringToHash: uuid, seed: seedA) <= 0.4, true) + XCTAssertLessThanOrEqual(hashString(stringToHash: uuid, seed: seedA), 0.4) } func testStringNonWorking() { - XCTAssertEqual(hashString(stringToHash: uuid, seed: seedB) > 0.4, true) + XCTAssertGreaterThan(hashString(stringToHash: uuid, seed: seedB), 0.4) } func testInt() { - XCTAssertEqual(hashInt(intToHash: 42, seed: seedB) > 0.4, true) + XCTAssertGreaterThan(hashInt(intToHash: 42, seed: seedB), 0.4) } func testFloat() { - XCTAssertEqual(hashFloat(floatToHash: 42.42, seed: seedB) > 0.4, false) + XCTAssertLessThanOrEqual(hashFloat(floatToHash: 42.42, seed: seedB), 0.4) } } diff --git a/clients/swift/Tests/VexillaClientTests/VexillaClientTests.swift b/clients/swift/Tests/VexillaClientTests/VexillaClientTests.swift index 6caebdf..4568b90 100644 --- a/clients/swift/Tests/VexillaClientTests/VexillaClientTests.swift +++ b/clients/swift/Tests/VexillaClientTests/VexillaClientTests.swift @@ -9,94 +9,46 @@ final class VexillaClientTests: XCTestCase { var baseUrl = "" var vexillaClient: VexillaClient? = nil + private func callAPI(url: URL) async throws -> Data { + let request = URLRequest(url: url) + + return try await withCheckedThrowingContinuation { continuation in + URLSession.shared.dataTask(with: request) { data, _, error in + if error != nil { + return continuation.resume(throwing: "could not fetch manifest") + } + return continuation.resume(returning: data ?? Data()) + }.resume() + } + } + override func setUp() async throws { let baseHost = ProcessInfo.processInfo.environment["TEST_SERVER_HOST"] ?? "localhost:3000" let baseUrl = "http://\(baseHost)" var vexillaClient = VexillaClient(environment: "dev", baseUrl: baseUrl, instanceId: "b7e91cc5-ec76-4ec3-9c1c-075032a13a1a") - let manifest: Manifest = try await vexillaClient.getManifest { urlString -> String in - print("WTF: ", urlString) - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch manifest") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + let manifest: Manifest = try await vexillaClient.getManifest(fetch: callAPI) vexillaClient.setManifest(manifest: manifest) - try await vexillaClient.syncManifest { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch manifest") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient.syncManifest(fetch: callAPI) self.vexillaClient = vexillaClient } func testGradual() async throws { - let gradualGroup: Group = try await vexillaClient!.getFlags(groupNameOrId: "Gradual") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch gradual group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + let gradualGroup: Group = try await vexillaClient!.getFlags(groupNameOrId: "Gradual", fetch: callAPI) try vexillaClient!.setFlags(groupNameOrId: "Gradual", group: gradualGroup) - try await vexillaClient!.syncFlags(groupNameOrId: "Gradual") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch gradual group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient!.syncFlags(groupNameOrId: "Gradual", fetch: callAPI) XCTAssertTrue(try vexillaClient!.should(groupNameOrId: "Gradual", featureNameOrId: "testingWorkingGradual")) XCTAssertFalse(try vexillaClient!.should(groupNameOrId: "Gradual", featureNameOrId: "testingNonWorkingGradual")) } func testSelective() async throws { - try await vexillaClient!.syncFlags(groupNameOrId: "Selective") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch selective group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient!.syncFlags(groupNameOrId: "Selective", fetch: callAPI) XCTAssertTrue(try vexillaClient!.should(groupNameOrId: "Selective", featureNameOrId: "String")) @@ -111,41 +63,17 @@ final class VexillaClientTests: XCTestCase { } func testValue() async throws { - try await vexillaClient!.syncFlags(groupNameOrId: "Value") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch value group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient!.syncFlags(groupNameOrId: "Value", fetch: callAPI) XCTAssertEqual("foo", try vexillaClient!.valueString(groupNameOrId: "Value", featureNameOrId: "String", defaultString: "")) - XCTAssertEqual(42, try vexillaClient!.valueInt(groupNameOrId: "Value", featureNameOrId: "Integer", defaultInt32: 0)) + XCTAssertEqual(42, try vexillaClient!.valueInt32(groupNameOrId: "Value", featureNameOrId: "Integer", defaultInt32: 0)) XCTAssertEqual(42, try vexillaClient!.valueInt64(groupNameOrId: "Value", featureNameOrId: "Integer", defaultInt64: 0)) XCTAssertEqual(42.42, try vexillaClient!.valueFloat(groupNameOrId: "Value", featureNameOrId: "Float", defaultFloat32: 0.0)) XCTAssertEqual(42.42, try vexillaClient!.valueFloat64(groupNameOrId: "Value", featureNameOrId: "Float", defaultFloat64: 0.0)) - try await vexillaClient!.syncFlags(groupNameOrId: "Scheduled") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch scheduled group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient!.syncFlags(groupNameOrId: "Scheduled", fetch: callAPI) XCTAssertFalse(try vexillaClient!.should(groupNameOrId: "Scheduled", featureNameOrId: "beforeGlobal")) XCTAssertTrue(try vexillaClient!.should(groupNameOrId: "Scheduled", featureNameOrId: "duringGlobal")) @@ -161,19 +89,7 @@ final class VexillaClientTests: XCTestCase { } func testScheduled() async throws { - try await vexillaClient!.syncFlags(groupNameOrId: "Scheduled") { urlString -> String in - let url = URL(string: urlString)! - let request = URLRequest(url: url) - - return try await withCheckedThrowingContinuation { continuation in - URLSession.shared.dataTask(with: request) { data, _, error in - if error != nil { - return continuation.resume(throwing: "could not fetch scheduled group") - } - return continuation.resume(returning: String(decoding: data!, as: UTF8.self)) - }.resume() - } - } + try await vexillaClient!.syncFlags(groupNameOrId: "Scheduled", fetch: callAPI) XCTAssertFalse(try vexillaClient!.should(groupNameOrId: "Scheduled", featureNameOrId: "beforeGlobal")) XCTAssertTrue(try vexillaClient!.should(groupNameOrId: "Scheduled", featureNameOrId: "duringGlobal"))