diff --git a/components/nimbus/ios/Nimbus/FeatureHolder.swift b/components/nimbus/ios/Nimbus/FeatureHolder.swift index e5d67b940e..0c67c69568 100644 --- a/components/nimbus/ios/Nimbus/FeatureHolder.swift +++ b/components/nimbus/ios/Nimbus/FeatureHolder.swift @@ -12,7 +12,7 @@ public typealias GetSdk = () -> FeaturesInterface? /// /// There are methods useful for testing, and more advanced uses: these all start with `with`. /// -public class FeatureHolder { +public class FeatureHolder { private let lock = NSLock() private var cachedValue: T? @@ -56,7 +56,9 @@ public class FeatureHolder { /// Send an exposure event for this feature. This should be done when the user is shown the feature, and may change /// their behavior because of it. public func recordExposure() { - getSdk()?.recordExposureEvent(featureId: featureId, experimentSlug: nil) + if !value().isModified() { + getSdk()?.recordExposureEvent(featureId: featureId, experimentSlug: nil) + } } /// Send an exposure event for this feature, in the given experiment. @@ -69,7 +71,9 @@ public class FeatureHolder { /// /// - Parameter slug the experiment identifier, likely derived from the {value}. public func recordExperimentExposure(slug: String) { - getSdk()?.recordExposureEvent(featureId: featureId, experimentSlug: slug) + if !value().isModified() { + getSdk()?.recordExposureEvent(featureId: featureId, experimentSlug: slug) + } } /// Send a malformed feature event for this feature. @@ -130,3 +134,26 @@ public class FeatureHolder { create = initializer } } + +/// A bare-bones interface for the FML generated objects. +public protocol FMLObjectInterface {} + +/// A bare-bones interface for the FML generated features. +/// +/// App developers should use the generated concrete classes, which +/// implement this interface. +/// +public protocol FMLFeatureInterface: FMLObjectInterface { + /// A test if the feature configuration has been modified somehow, invalidating any experiment + /// that uses it. + /// + /// This may be `true` if a `pref-key` has been set in the feature manifest and the user has + /// set that preference. + func isModified() -> Bool +} + +public extension FMLFeatureInterface { + func isModified() -> Bool { + return false + } +} diff --git a/components/support/nimbus-fml/src/backends/swift/templates/FeatureTemplate.swift b/components/support/nimbus-fml/src/backends/swift/templates/FeatureTemplate.swift index 63afe0aa03..42873bcefc 100644 --- a/components/support/nimbus-fml/src/backends/swift/templates/FeatureTemplate.swift +++ b/components/support/nimbus-fml/src/backends/swift/templates/FeatureTemplate.swift @@ -1,3 +1,28 @@ {%- import "macros.swift" as swift %} {%- let inner = self.inner() %} -{% call swift::render_class(inner) %} \ No newline at end of file +{%- let class_name = inner.name()|class_name -%} +{% call swift::render_class(inner) %} + +{%- if inner.has_prefs() %} + +extension {{ class_name }}: FMLFeatureInterface { + public func isModified() -> Bool { + guard let prefs = {% call swift::prefs() %} else { + return false + } + let keys = [ + {%- for p in inner.props() %} + {%- if p.has_prefs() %} + {{ p.pref_key().unwrap()|quoted }}, + {%- endif %} + {%- endfor %} + ] + if let _ = keys.first(where: { prefs.object(forKey: $0) != nil }) { + return true + } + return false + } +} +{%- else %} +extension {{ class_name }}: FMLFeatureInterface {} +{%- endif %} diff --git a/components/support/nimbus-fml/src/backends/swift/templates/ObjectTemplate.swift b/components/support/nimbus-fml/src/backends/swift/templates/ObjectTemplate.swift index dfb6788393..ced91997e2 100644 --- a/components/support/nimbus-fml/src/backends/swift/templates/ObjectTemplate.swift +++ b/components/support/nimbus-fml/src/backends/swift/templates/ObjectTemplate.swift @@ -1,7 +1,7 @@ {%- import "macros.swift" as swift %} {%- let inner = self.inner() %} {% call swift::render_class(inner) -%} -{% let class_name = inner.name()|class_name -%} +{%- let class_name = inner.name()|class_name %} public extension {{class_name}} { func _mergeWith(_ defaults: {{class_name}}?) -> {{class_name}} { diff --git a/components/support/nimbus-fml/src/backends/swift/templates/macros.swift b/components/support/nimbus-fml/src/backends/swift/templates/macros.swift index 92860229b7..4c87c7c909 100644 --- a/components/support/nimbus-fml/src/backends/swift/templates/macros.swift +++ b/components/support/nimbus-fml/src/backends/swift/templates/macros.swift @@ -13,7 +13,7 @@ {% let class_name = inner.name()|class_name -%} {{ inner.doc()|comment("") }} -public class {{class_name}} { +public class {{class_name}}: FMLObjectInterface { private let _variables: Variables private let _defaults: Defaults private let _prefs: UserDefaults? @@ -71,7 +71,7 @@ public class {{class_name}} { `has_prefs()` checks if the type can be got from UserDefaults. #} - if let {{ prefs }} = self._prefs, + if let {{ prefs }} = {% call prefs() %}, let {{ prop_swift }} = {{ prefs }}.object(forKey: {{ key|quoted }}) as? {{ type_swift }} { return {{ prop_swift }} } @@ -81,4 +81,6 @@ public class {{class_name}} { {% endfor %} } -{% endmacro %}} +{%- endmacro %}} + +{% macro prefs() %}self._prefs{% endmacro %} diff --git a/components/support/nimbus-fml/test/pref_overrides.swift b/components/support/nimbus-fml/test/pref_overrides.swift index 5e258a2efb..73428b7bb3 100644 --- a/components/support/nimbus-fml/test/pref_overrides.swift +++ b/components/support/nimbus-fml/test/pref_overrides.swift @@ -41,6 +41,7 @@ assert(feature0.myBoolean == false) assert(feature0.myInt == 0) assert(feature0.myString == "from manifest") assert(feature0.myText == "from manifest") +assert(!feature0.isModified()) // Now test that JSON still has an effect. let prefs = UserDefaults() @@ -66,6 +67,7 @@ assert(feature.myBoolean == false) assert(feature.myInt == 100) assert(feature.myString == "from json") assert(feature.myText == "from json") +assert(!feature.isModified()) // Now set with prefs. @@ -78,3 +80,4 @@ assert(feature.myBoolean == true) assert(feature.myInt == 42) assert(feature.myString == "from pref") assert(feature.myText == "from pref") +assert(feature.isModified()) diff --git a/components/support/nimbus-fml/test/threadsafe_feature_holder.swift b/components/support/nimbus-fml/test/threadsafe_feature_holder.swift index d1e391c404..3dc954ad6a 100644 --- a/components/support/nimbus-fml/test/threadsafe_feature_holder.swift +++ b/components/support/nimbus-fml/test/threadsafe_feature_holder.swift @@ -5,6 +5,13 @@ import FeatureManifest import Foundation +class Feature: FMLFeatureInterface { + let string: String + init(_ string: String) { + self.string = string + } +} + let queue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 5 @@ -12,7 +19,7 @@ let queue: OperationQueue = { }() let api: FeaturesInterface = HardcodedNimbusFeatures(with: ["test-feature-holder": "{}"]) -let holder = FeatureHolder({ api }, featureId: "test-feature-holder") { _, _ in "NO CRASH" } +let holder = FeatureHolder({ api }, featureId: "test-feature-holder") { _, _ in Feature("NO CRASH") } for _ in 1 ..< 10000 { queue.addOperation {