Skip to content

Commit

Permalink
feat: Introduce variation method with generic return types (#342)
Browse files Browse the repository at this point in the history
Customers using a JSON flag often prefer the evaluation result return a
custom object instead of an LDValue. This commit supports that by
introducing a new `LDValueDecoder` (copied and lightly modified from the
Swift Core Libs `JSONDecoder` implementation).

This decoder allows decoding into arbitrary types directly from an
LDValue, without paying the penalty for an intermediate JSON encoding
step (i.e. LDValue -> JSON str -> Custom Type).
  • Loading branch information
keelerm84 committed Feb 15, 2024
1 parent 7ba4397 commit 7ff2ffb
Show file tree
Hide file tree
Showing 5 changed files with 962 additions and 40 deletions.
14 changes: 14 additions & 0 deletions LaunchDarkly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@
A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; };
A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; };
A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; };
A3470C372B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; };
A3470C382B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; };
A3470C392B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; };
A3470C3A2B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; };
A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; };
A358D6D12A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; };
A358D6D22A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; };
Expand Down Expand Up @@ -256,6 +260,7 @@
A380B09A2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A380B09B2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A380B09C2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; };
A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */; };
B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; };
B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; };
B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; };
Expand Down Expand Up @@ -455,6 +460,7 @@
A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = "<group>"; };
A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = "<group>"; };
A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = "<group>"; };
A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoder.swift; sourceTree = "<group>"; };
A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = "<group>"; };
A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterChainBase.swift; sourceTree = "<group>"; };
A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationInfoEnvironmentReporter.swift; sourceTree = "<group>"; };
Expand All @@ -470,6 +476,7 @@
A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = "<group>"; };
A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = "<group>"; };
A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoderSpec.swift; sourceTree = "<group>"; };
B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = "<group>"; };
B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = "<group>"; };
B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -641,6 +648,7 @@
83E2E2071F9FF9A0007514E9 /* Extensions */,
835E1D341F63332C00184DB4 /* ObjectiveC */,
83B6C4B71F4DE78B0055351C /* Support */,
A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */,
);
name = LaunchDarkly;
path = LaunchDarkly/LaunchDarkly;
Expand All @@ -658,6 +666,7 @@
83D17EA81FCDA16300B2823C /* Extensions */,
8354EFD21F22491C00C05156 /* Info.plist */,
B4265EB024E7390C001CFD2C /* TestUtil.swift */,
A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */,
);
name = LaunchDarklyTests;
path = LaunchDarkly/LaunchDarklyTests;
Expand Down Expand Up @@ -1244,6 +1253,7 @@
8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */,
C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */,
8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */,
A3470C3A2B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
831188592113AE1200D77CB5 /* FlagStore.swift in Sources */,
C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */,
Expand Down Expand Up @@ -1311,6 +1321,7 @@
A31088192837DC0400184942 /* Reference.swift in Sources */,
831EF34E20655E730001C643 /* Event.swift in Sources */,
A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */,
A3470C392B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */,
831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */,
Expand Down Expand Up @@ -1377,6 +1388,7 @@
835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */,
8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */,
83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */,
A3470C372B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */,
831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */,
8354EFE11F26380700C05156 /* LDConfig.swift in Sources */,
Expand Down Expand Up @@ -1458,6 +1470,7 @@
830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */,
831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */,
838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */,
A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */,
A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */,
837406D421F760640087B22B /* LDTimerSpec.swift in Sources */,
832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */,
Expand Down Expand Up @@ -1493,6 +1506,7 @@
83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */,
A358D6F02A4DE9EB00270C60 /* WatchOSEnvironmentReporter.swift in Sources */,
831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */,
A3470C382B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */,
C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */,
83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */,
C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */,
Expand Down
89 changes: 50 additions & 39 deletions LaunchDarkly/LaunchDarkly/LDClientVariation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,44 @@ extension LDClient {
variationDetailInternal(flagKey, defaultValue, needsReason: true)
}

private func variationDetailInternal<T: LDValueConvertible>(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail<T> {
/**
Returns the value of a feature flag for a given flag key, converting the raw JSON value into a type of your specification.
- parameter forKey: the unique feature key for the feature flag.
- parameter defaultValue: the default value for if the flag value is unavailable.
- returns: the variation for the selected context, or `defaultValue` if the flag is not available.
*/
public func variation<T>(forKey flagKey: LDFlagKey, defaultValue: T) -> T where T: LDValueConvertible, T: Decodable {
return variationDetailInternal(flagKey, defaultValue, needsReason: false).value
}

/**
Returns the value of a feature flag for a given flag key, converting the raw JSON value into a type
of your specifification, and including it in an object that also describes the way the value was
determined.
- parameter forKey: the unique feature key for the feature flag.
- parameter defaultValue: the default value for if the flag value is unavailable.
- returns: an `LDEvaluationDetail` object
*/
public func variationDetail<T>(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail<T> where T: LDValueConvertible, T: Decodable {
return variationDetailInternal(flagKey, defaultValue, needsReason: true)
}

private func variationDetailInternal<T>(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail<T> where T: Decodable, T: LDValueConvertible {
var result: LDEvaluationDetail<T>
let featureFlag = flagStore.featureFlag(for: flagKey)
if let featureFlag = featureFlag {
if featureFlag.value == .null {
result = LDEvaluationDetail(value: defaultValue, variationIndex: featureFlag.variation, reason: featureFlag.reason)
} else if let convertedValue = T(fromLDValue: featureFlag.value) {
result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason)
} else {
result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"])
do {
let convertedValue = try LDValueDecoder().decode(T.self, from: featureFlag.value)
result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason)
} catch let error {
os_log("%s type conversion error %s: failed converting %s to type %s", log: config.logger, type: .debug, typeName(and: #function), String(describing: error), String(describing: featureFlag.value), String(describing: T.self))
result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"])
}
}
} else {
os_log("%s Unknown feature flag %s; returning default value", log: config.logger, type: .debug, typeName(and: #function), flagKey.description)
Expand All @@ -144,65 +172,48 @@ extension LDClient {
}
}

private protocol LDValueConvertible {
init?(fromLDValue: LDValue)
/**
Protocol indicting a type can be converted into an LDValue.
Types used with the `LDClient.variation(forKey: defaultValue:)` or `LDClient.variationDetail(forKey: detailValue:)`
methods are required to implement this protocol. This protocol has already been implemented for Bool, Int, Double, String,
and LDValue types.
This allows custom types as evaluation result types while retaining the LDValue type throughout the event processing system.
*/
public protocol LDValueConvertible {
/**
Return an LDValue representation of this instance.
*/
func toLDValue() -> LDValue
}

extension Bool: LDValueConvertible {
init?(fromLDValue value: LDValue) {
guard case .bool(let value) = value
else { return nil }
self = value
}

func toLDValue() -> LDValue {
public func toLDValue() -> LDValue {
return .bool(self)
}
}

extension Int: LDValueConvertible {
init?(fromLDValue value: LDValue) {
guard case .number(let value) = value, let intValue = Int(exactly: value.rounded())
else { return nil }
self = intValue
}

func toLDValue() -> LDValue {
public func toLDValue() -> LDValue {
return .number(Double(self))
}
}

extension Double: LDValueConvertible {
init?(fromLDValue value: LDValue) {
guard case .number(let value) = value
else { return nil }
self = value
}

func toLDValue() -> LDValue {
public func toLDValue() -> LDValue {
return .number(self)
}
}

extension String: LDValueConvertible {
init?(fromLDValue value: LDValue) {
guard case .string(let value) = value
else { return nil }
self = value
}

func toLDValue() -> LDValue {
public func toLDValue() -> LDValue {
return .string(self)
}
}

extension LDValue: LDValueConvertible {
init?(fromLDValue value: LDValue) {
self = value
}

func toLDValue() -> LDValue {
public func toLDValue() -> LDValue {
return self
}
}
Loading

0 comments on commit 7ff2ffb

Please sign in to comment.