Skip to content

Commit

Permalink
Add isModified() in swift to detect if users have modified the feature
Browse files Browse the repository at this point in the history
  • Loading branch information
jhugman committed Oct 11, 2023
1 parent ff4d7dd commit 9643ee2
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 9 deletions.
33 changes: 30 additions & 3 deletions components/nimbus/ios/Nimbus/FeatureHolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
public class FeatureHolder<T: FMLFeatureInterface> {
private let lock = NSLock()
private var cachedValue: T?

Expand Down Expand Up @@ -56,7 +56,9 @@ public class FeatureHolder<T> {
/// 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.
Expand All @@ -69,7 +71,9 @@ public class FeatureHolder<T> {
///
/// - 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.
Expand Down Expand Up @@ -130,3 +134,26 @@ public class FeatureHolder<T> {
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
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
{%- import "macros.swift" as swift %}
{%- let inner = self.inner() %}
{% call swift::render_class(inner) %}
{%- 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 %}
Original file line number Diff line number Diff line change
@@ -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}} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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 }}
}
Expand All @@ -81,4 +81,6 @@ public class {{class_name}} {
{% endfor %}
}

{% endmacro %}}
{%- endmacro %}}

{% macro prefs() %}self._prefs{% endmacro %}
3 changes: 3 additions & 0 deletions components/support/nimbus-fml/test/pref_overrides.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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.

Expand All @@ -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())
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@
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
return queue
}()

let api: FeaturesInterface = HardcodedNimbusFeatures(with: ["test-feature-holder": "{}"])
let holder = FeatureHolder<String>({ api }, featureId: "test-feature-holder") { _, _ in "NO CRASH" }
let holder = FeatureHolder<Feature>({ api }, featureId: "test-feature-holder") { _, _ in Feature("NO CRASH") }

for _ in 1 ..< 10000 {
queue.addOperation {
Expand Down

0 comments on commit 9643ee2

Please sign in to comment.