@@ -5,10 +5,47 @@ import Foundation
5
5
6
6
public typealias GetSdk = ( ) -> FeaturesInterface ?
7
7
8
- /// `FeatureHolder` is a class that unpacks a JSON object from the Nimbus SDK and transforms it into a useful
8
+ public protocol FeatureHolderInterface {
9
+ /// Send an exposure event for this feature. This should be done when the user is shown the feature, and may change
10
+ /// their behavior because of it.
11
+ func recordExposure( )
12
+
13
+ /// Send an exposure event for this feature, in the given experiment.
14
+ ///
15
+ /// If the experiment does not exist, or the client is not enrolled in that experiment, then no exposure event
16
+ /// is recorded.
17
+ ///
18
+ /// If you are not sure of the experiment slug, then this is _not_ the API you need: you should use
19
+ /// {recordExposure} instead.
20
+ ///
21
+ /// - Parameter slug the experiment identifier, likely derived from the ``value``.
22
+ func recordExperimentExposure( slug: String )
23
+
24
+ /// Send a malformed feature event for this feature.
25
+ ///
26
+ /// - Parameter partId an optional detail or part identifier to be attached to the event.
27
+ func recordMalformedConfiguration( with partId: String )
28
+
29
+ /// Is this feature the focus of an automated test.
30
+ ///
31
+ /// A utility flag to be used in conjunction with ``HardcodedNimbusFeatures``.
32
+ ///
33
+ /// It is intended for use for app-code to detect when the app is under test, and
34
+ /// take steps to make itself easier to test.
35
+ ///
36
+ /// These cases should be rare, and developers should look for other ways to test
37
+ /// code without relying on this facility.
38
+ ///
39
+ /// For example, a background worker might be scheduled to run every 24 hours, but
40
+ /// under test it would be desirable to run immediately, and only once.
41
+ func isUnderTest( ) -> Bool
42
+ }
43
+
44
+ /// ``FeatureHolder`` is a class that unpacks a JSON object from the Nimbus SDK and transforms it into a useful
9
45
/// type safe object, generated from a feature manifest (a `.fml.yaml` file).
10
46
///
11
- /// The two routinely useful methods are the `value()` and `recordExposure()` events.
47
+ /// The routinely useful methods to application developers are the ``value()`` and the event recording
48
+ /// methods of ``FeatureHolderInterface``.
12
49
///
13
50
/// There are methods useful for testing, and more advanced uses: these all start with `with`.
14
51
///
@@ -35,7 +72,7 @@ public class FeatureHolder<T: FMLFeatureInterface> {
35
72
/// result used for the configuration of the feature.
36
73
///
37
74
/// Some care is taken to cache the value, this is for performance critical uses of the API.
38
- /// It is possible to invalidate the cache with `FxNimbus.invalidateCachedValues()` or `with(cachedValue: nil)`.
75
+ /// It is possible to invalidate the cache with `FxNimbus.invalidateCachedValues()` or `` with(cachedValue: nil)` `.
39
76
public func value( ) -> T {
40
77
lock. lock ( )
41
78
defer { self . lock. unlock ( ) }
@@ -53,48 +90,53 @@ public class FeatureHolder<T: FMLFeatureInterface> {
53
90
return v
54
91
}
55
92
56
- /// Send an exposure event for this feature. This should be done when the user is shown the feature, and may change
57
- /// their behavior because of it.
93
+ /// This overwrites the cached value with the passed one.
94
+ ///
95
+ /// This is most likely useful during testing only.
96
+ public func with( cachedValue value: T ? ) {
97
+ lock. lock ( )
98
+ defer { self . lock. unlock ( ) }
99
+ cachedValue = value
100
+ }
101
+
102
+ /// This resets the SDK and clears the cached value.
103
+ ///
104
+ /// This is especially useful at start up and for imported features.
105
+ public func with( sdk: @escaping ( ) -> FeaturesInterface ? ) {
106
+ lock. lock ( )
107
+ defer { self . lock. unlock ( ) }
108
+ getSdk = sdk
109
+ cachedValue = nil
110
+ }
111
+
112
+ /// This changes the mapping between a ``Variables`` and the feature configuration object.
113
+ ///
114
+ /// This is most likely useful during testing and other generated code.
115
+ public func with( initializer: @escaping ( Variables , UserDefaults ? ) -> T ) {
116
+ lock. lock ( )
117
+ defer { self . lock. unlock ( ) }
118
+ cachedValue = nil
119
+ create = initializer
120
+ }
121
+ }
122
+
123
+ extension FeatureHolder : FeatureHolderInterface {
58
124
public func recordExposure( ) {
59
125
if !value( ) . isModified ( ) {
60
126
getSdk ( ) ? . recordExposureEvent ( featureId: featureId, experimentSlug: nil )
61
127
}
62
128
}
63
129
64
- /// Send an exposure event for this feature, in the given experiment.
65
- ///
66
- /// If the experiment does not exist, or the client is not enrolled in that experiment, then no exposure event
67
- /// is recorded.
68
- ///
69
- /// If you are not sure of the experiment slug, then this is _not_ the API you need: you should use
70
- /// {recordExposure} instead.
71
- ///
72
- /// - Parameter slug the experiment identifier, likely derived from the {value}.
73
130
public func recordExperimentExposure( slug: String ) {
74
131
if !value( ) . isModified ( ) {
75
132
getSdk ( ) ? . recordExposureEvent ( featureId: featureId, experimentSlug: slug)
76
133
}
77
134
}
78
135
79
- /// Send a malformed feature event for this feature.
80
- ///
81
- /// - Parameter partId an optional detail or part identifier to be attached to the event.
82
136
public func recordMalformedConfiguration( with partId: String = " " ) {
83
137
getSdk ( ) ? . recordMalformedConfiguration ( featureId: featureId, with: partId)
84
138
}
85
139
86
- /// Is this feature the focus of an automated test.
87
- ///
88
- /// A utility flag to be used in conjunction with {HardcodedNimbusFeatures}.
89
- ///
90
- /// It is intended for use for app-code to detect when the app is under test, and
91
- /// take steps to make itself easier to test.
92
- ///
93
- /// These cases should be rare, and developers should look for other ways to test
94
- /// code without relying on this facility.
95
- ///
96
- /// For example, a background worker might be scheduled to run every 24 hours, but
97
- /// under test it would be desirable to run immediately, and only once.
98
140
public func isUnderTest( ) -> Bool {
99
141
lock. lock ( )
100
142
defer { self . lock. unlock ( ) }
@@ -104,39 +146,53 @@ public class FeatureHolder<T: FMLFeatureInterface> {
104
146
}
105
147
return features. has ( featureId: featureId)
106
148
}
149
+ }
107
150
108
- /// This overwrites the cached value with the passed one.
109
- ///
110
- /// This is most likely useful during testing only.
111
- public func with( cachedValue value: T ? ) {
112
- lock. lock ( )
113
- defer { self . lock. unlock ( ) }
114
- cachedValue = value
151
+ /// Swift generics don't allow us to do wildcards, which means implementing a
152
+ /// ``getFeature(featureId: String) -> FeatureHolder<*>`` unviable.
153
+ ///
154
+ /// To implement such a method, we need a wrapper object that gets the value, and forwards
155
+ /// all other calls onto an inner ``FeatureHolder``.
156
+ public class FeatureHolderAny {
157
+ let inner : FeatureHolderInterface
158
+ let innerValue : FMLFeatureInterface
159
+ init < T> ( wrapping holder: FeatureHolder < T > ) {
160
+ inner = holder
161
+ innerValue = holder. value ( )
115
162
}
116
163
117
- /// This resets the SDK and clears the cached value.
118
- ///
119
- /// This is especially useful at start up and for imported features.
120
- public func with( sdk: @escaping ( ) -> FeaturesInterface ? ) {
121
- lock. lock ( )
122
- defer { self . lock. unlock ( ) }
123
- getSdk = sdk
124
- cachedValue = nil
164
+ public func value( ) -> FMLFeatureInterface {
165
+ innerValue
125
166
}
126
167
127
- /// This changes the mapping between a `Variables` and the feature configuration object .
168
+ /// Returns a JSON string representing the complete configuration.
128
169
///
129
- /// This is most likely useful during testing and other generated code.
130
- public func with( initializer: @escaping ( Variables , UserDefaults ? ) -> T ) {
131
- lock. lock ( )
132
- defer { self . lock. unlock ( ) }
133
- cachedValue = nil
134
- create = initializer
170
+ /// A convenience for `self.value().toJSONString()`.
171
+ public func toJSONString( ) -> String {
172
+ innerValue. toJSONString ( )
173
+ }
174
+ }
175
+
176
+ extension FeatureHolderAny : FeatureHolderInterface {
177
+ public func recordExposure( ) {
178
+ inner. recordExposure ( )
179
+ }
180
+
181
+ public func recordExperimentExposure( slug: String ) {
182
+ inner. recordExperimentExposure ( slug: slug)
183
+ }
184
+
185
+ public func recordMalformedConfiguration( with partId: String ) {
186
+ inner. recordMalformedConfiguration ( with: partId)
187
+ }
188
+
189
+ public func isUnderTest( ) -> Bool {
190
+ inner. isUnderTest ( )
135
191
}
136
192
}
137
193
138
194
/// A bare-bones interface for the FML generated objects.
139
- public protocol FMLObjectInterface { }
195
+ public protocol FMLObjectInterface : Encodable { }
140
196
141
197
/// A bare-bones interface for the FML generated features.
142
198
///
@@ -150,10 +206,19 @@ public protocol FMLFeatureInterface: FMLObjectInterface {
150
206
/// This may be `true` if a `pref-key` has been set in the feature manifest and the user has
151
207
/// set that preference.
152
208
func isModified( ) -> Bool
209
+
210
+ /// Returns a string representation of the complete feature configuration in JSON format.
211
+ func toJSONString( ) -> String
153
212
}
154
213
155
214
public extension FMLFeatureInterface {
156
215
func isModified( ) -> Bool {
157
216
return false
158
217
}
218
+
219
+ func toJSONString( ) -> String {
220
+ let encoder = JSONEncoder ( )
221
+ let data = try ! encoder. encode ( self )
222
+ return String ( decoding: data, as: UTF8 . self)
223
+ }
159
224
}
0 commit comments