Skip to content

Commit

Permalink
feat: get variation details by variation type (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
duyhungtnn authored Sep 5, 2024
1 parent 0b6fa89 commit 5a79914
Show file tree
Hide file tree
Showing 15 changed files with 1,611 additions and 62 deletions.
47 changes: 36 additions & 11 deletions Bucketeer/Sources/Internal/Model/Evaluation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct Evaluation: Hashable, Codable {
struct Evaluation: Equatable, Codable, Hashable {
let id: String
let featureId: String
let featureVersion: Int
Expand All @@ -13,28 +13,53 @@ struct Evaluation: Hashable, Codable {

extension Evaluation {
func getVariationValue<T>(defaultValue: T, logger: Logger?) -> T {
let value = self.variationValue
return getVariationValue(logger: logger) ?? defaultValue
}

func getVariationValue<T>(logger: Logger?) -> T? {
return variationValue.getVariationValue(logger: logger)
}
}

extension String {
func getVariationValue<T>(logger: Logger?) -> T? {
let value = self
let anyValue: Any?
switch defaultValue {
case is String:
switch T.self {
case is String.Type:
anyValue = value
case is Int:
anyValue = Int(value)
case is Double:
case is Int.Type:
if let doubleValue = Double(value) {
anyValue = Int(doubleValue)
} else {
anyValue = nil
}
case is Double.Type:
anyValue = Double(value)
case is Bool:
case is Bool.Type:
anyValue = Bool(value)
case is [String: AnyHashable]:
case is [String: AnyHashable].Type:
let data = value.data(using: .utf8) ?? Data()
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: AnyHashable] ?? [:]
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: AnyHashable]
anyValue = json
case is BKTValue.Type:
anyValue = getVariationBKTValue()
default:
anyValue = value
}
guard let typedValue = anyValue as? T else {
logger?.debug(message: "getVariation returns null reason: failed to cast \(value)")
return defaultValue
return nil
}
return typedValue
}

func getVariationBKTValue() -> BKTValue {
let value = self
let data = value.data(using: .utf8) ?? Data()
if let valueResult = (try? JSONDecoder().decode(BKTValue.self, from: data)), valueResult != .null {
return valueResult
}
return .string(self)
}
}
2 changes: 1 addition & 1 deletion Bucketeer/Sources/Internal/Model/UserEvaluations.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct UserEvaluations: Hashable, Codable {
struct UserEvaluations: Hashable, Equatable, Codable {
let id: String
let evaluations: [Evaluation]
let createdAt: String
Expand Down
68 changes: 54 additions & 14 deletions Bucketeer/Sources/Public/BKTClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,30 @@ public class BKTClient {
self.component = ComponentImpl(dataModule: dataModule)
}

func getVariationValue<T>(featureId: String, defaultValue: T) -> T {
component.config.logger?.debug(message: "BKTClient.getVariation(featureId = \(featureId), defaultValue = \(defaultValue) called")
func getBKTEvaluationDetails<T:Equatable>(featureId: String, defaultValue: T) -> BKTEvaluationDetails<T> {
component.config.logger?.debug(message: "BKTClient.getVariationDetail(featureId = \(featureId), defaultValue = \(defaultValue) called")
let raw = component.evaluationInteractor.getLatest(
userId: component.userHolder.userId,
featureId: featureId
)
let user = component.userHolder.user
let featureTag = component.config.featureTag
guard let raw = raw else {

guard let raw = raw, let value: T = raw.getVariationValue(
logger: component.config.logger
) else {
execute {
try self.component.eventInteractor.trackDefaultEvaluationEvent(
featureTag: featureTag,
user: user,
featureId: featureId
)
}
return defaultValue
return BKTEvaluationDetails.newDefaultInstance(
featureId: featureId,
userId: user.id,
defaultValue: defaultValue
)
}
execute {
try self.component.eventInteractor.trackEvaluationEvent(
Expand All @@ -37,9 +44,15 @@ public class BKTClient {
evaluation: raw
)
}
return raw.getVariationValue(
defaultValue: defaultValue,
logger: component.config.logger

return BKTEvaluationDetails(
featureId: featureId,
featureVersion: raw.featureVersion,
userId: raw.userId,
variationId: raw.variationId,
variationName: raw.variationName,
variationValue: value,
reason: BKTEvaluationDetails<T>.Reason.fromString(value: raw.reason.type.rawValue)
)
}

Expand Down Expand Up @@ -126,24 +139,50 @@ extension BKTClient {
}
}

public func stringVariation(featureId: String, defaultValue: String) -> String {
return getVariationValue(featureId: featureId, defaultValue: defaultValue)
public func boolVariation(featureId: String, defaultValue: Bool) -> Bool {
return boolVariationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func boolVariationDetails(featureId: String, defaultValue: Bool) -> BKTEvaluationDetails<Bool> {
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue)
}

public func intVariation(featureId: String, defaultValue: Int) -> Int {
return getVariationValue(featureId: featureId, defaultValue: defaultValue)
return intVariationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func intVariationDetails(featureId: String, defaultValue: Int) -> BKTEvaluationDetails<Int> {
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue)
}

public func doubleVariation(featureId: String, defaultValue: Double) -> Double {
return getVariationValue(featureId: featureId, defaultValue: defaultValue)
return doubleVariationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func boolVariation(featureId: String, defaultValue: Bool) -> Bool {
return getVariationValue(featureId: featureId, defaultValue: defaultValue)
public func doubleVariationDetails(featureId: String, defaultValue: Double) -> BKTEvaluationDetails<Double> {
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue)
}

public func stringVariation(featureId: String, defaultValue: String) -> String {
return stringVariationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func stringVariationDetails(featureId: String, defaultValue: String) -> BKTEvaluationDetails<String> {
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue)
}

public func objectVariation(featureId: String, defaultValue: BKTValue) -> BKTValue {
return objectVariationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func objectVariationDetails(featureId: String, defaultValue:BKTValue)
-> BKTEvaluationDetails<BKTValue> {
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue)
}

@available(*, deprecated, message: "use objectVariation(featureId:, defaultValue:) instead")
public func jsonVariation(featureId: String, defaultValue: [String: AnyHashable]) -> [String: AnyHashable] {
return getVariationValue(featureId: featureId, defaultValue: defaultValue)
return getBKTEvaluationDetails(featureId: featureId, defaultValue: defaultValue).variationValue
}

public func track(goalId: String, value: Double = 0.0) {
Expand Down Expand Up @@ -200,6 +239,7 @@ extension BKTClient {
}
}

@available(*, deprecated, message: "use stringVariationDetails(featureId:, defaultValue:) instead")
public func evaluationDetails(featureId: String) -> BKTEvaluation? {
let userId = self.component.userHolder.userId
let evaluation = self.component.evaluationInteractor.getLatest(userId: userId, featureId: featureId)
Expand Down
2 changes: 2 additions & 0 deletions Bucketeer/Sources/Public/BKTEvaluation.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation

@available(*, deprecated, message: "Use BKTEvaluationDetails<T> instead")
public struct BKTEvaluation: Equatable {
public let id: String
public let featureId: String
Expand All @@ -10,6 +11,7 @@ public struct BKTEvaluation: Equatable {
public let variationValue: String
public let reason: Reason

@available(*, deprecated, message: "Use BKTEvaluationDetails<T>.Reason instead")
public enum Reason: String, Codable, Hashable {
case target = "TARGET"
case rule = "RULE"
Expand Down
62 changes: 62 additions & 0 deletions Bucketeer/Sources/Public/BKTEvaluationDetails.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation

public struct BKTEvaluationDetails<T:Equatable>: Equatable {
public let featureId: String
public let featureVersion: Int
public let userId: String
public let variationId: String
public let variationName: String
public let variationValue: T
public let reason: Reason

public enum Reason: String, Codable, Hashable {
case target = "TARGET"
case rule = "RULE"
case `default` = "DEFAULT"
case client = "CLIENT"
case offVariation = "OFF_VARIATION"
case prerequisite = "PREREQUISITE"

public static func fromString(value: String) -> Reason {
return Reason(rawValue: value) ?? .client
}
}

public static func == (lhs: BKTEvaluationDetails<T>, rhs: BKTEvaluationDetails<T>) -> Bool {
return lhs.featureId == rhs.featureId &&
lhs.featureVersion == rhs.featureVersion &&
lhs.userId == rhs.userId &&
lhs.variationId == rhs.variationId &&
lhs.variationName == rhs.variationName &&
lhs.reason == rhs.reason &&
lhs.variationValue == rhs.variationValue
}

static func newDefaultInstance(featureId: String, userId: String, defaultValue: T) -> BKTEvaluationDetails<T> {
return BKTEvaluationDetails(
featureId: featureId,
featureVersion: 0,
userId: userId,
variationId: "",
variationName: "",
variationValue: defaultValue,
reason: .client
)
}

public init(featureId: String,
featureVersion: Int,
userId: String,
variationId: String,
variationName: String,
variationValue: T,
reason: Reason) {
self.featureId = featureId
self.featureVersion = featureVersion
self.userId = userId
self.variationId = variationId
self.variationName = variationName
self.variationValue = variationValue
self.reason = reason
}
}
118 changes: 118 additions & 0 deletions Bucketeer/Sources/Public/BKTValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Foundation

/**
* BKTValue Represents a JSON node value
*/
public enum BKTValue: Equatable, Codable, Hashable {
case boolean(Bool)
case string(String)
case number(Double)
case list([BKTValue])
case dictionary([String: BKTValue])
case null

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let doubleValue = try? container.decode(Double.self) {
self = .number(doubleValue)
} else if let boolValue = try? container.decode(Bool.self) {
self = .boolean(boolValue)
} else if let objectValue = try? container.decode([String: BKTValue].self) {
self = .dictionary(objectValue)
} else if let arrayValue = try? container.decode([BKTValue].self) {
self = .list(arrayValue)
} else if container.decodeNil() {
self = .null
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode BKTValue")
}
}

// Encode the JSON based on its type
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let value):
try container.encode(value)
case .number(let value):
try container.encode(value)
case .dictionary(let value):
try container.encode(value)
case .list(let value):
try container.encode(value)
case .boolean(let value):
try container.encode(value)
case .null:
try container.encodeNil()
}
}

public func asBoolean() -> Bool? {
if case let .boolean(bool) = self {
return bool
}

return nil
}

public func asString() -> String? {
if case let .string(string) = self {
return string
}

return nil
}

public func asInteger() -> Int? {
if case let .number(double) = self {
return Int(double)
}

return nil
}

public func asDouble() -> Double? {
if case let .number(double) = self {
return double
}

return nil
}

public func asList() -> [BKTValue]? {
if case let .list(values) = self {
return values
}

return nil
}

public func asDictionary() -> [String: BKTValue]? {
if case let .dictionary(values) = self {
return values
}

return nil
}
}

extension BKTValue: CustomStringConvertible {
public var description: String {
switch self {
case .boolean(let value):
return "\(value)"
case .string(let value):
return value
case .number(let value):
return "\(value)"
case .list(value: let values):
return "\(values.map { value in value.description })"
case .dictionary(value: let values):
return "\(values.mapValues { value in value.description })"
case .null:
return "null"
}
}
}
Loading

0 comments on commit 5a79914

Please sign in to comment.