/
Laboratory.kt
220 lines (187 loc) · 7.58 KB
/
Laboratory.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package io.mehow.laboratory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
/**
* High-level API for interaction with feature flags. It allows to read and write their values.
*/
public class Laboratory private constructor(builder: Builder) {
private val storage = builder.storage
private val defaultOptionFactory = builder.defaultOptionFactory?.let(::SafeDefaultOptionFactory)
@Deprecated(
message = "This method will be removed in 1.0.0. Use 'Laboratory.create()' instead.",
replaceWith = ReplaceWith("Laboratory.create(storage)"),
)
public constructor(storage: FeatureStorage) : this(Builder().apply { this.storage = storage })
/**
* Observes any changes to the input [Feature].
*/
public inline fun <reified T : Feature<T>> observe(): Flow<T> = observe(T::class.java)
/**
* Observes any changes to the input [Feature].
*/
public fun <T : Feature<T>> observe(feature: Class<T>): Flow<T> {
val options = feature.options
val defaultOption = getDefaultOption(feature)
return storage.observeFeatureName(feature).map { featureName ->
val expectedName = featureName ?: defaultOption.name
options.firstOrNull { it.name == expectedName } ?: defaultOption
}
}
/**
* Returns the current value of the input [Feature].
*/
public suspend inline fun <reified T : Feature<T>> experiment(): T = experiment(T::class.java)
/**
* Returns the current value of the input [Feature]. Warning – this call can block the calling thread.
*
* @see BlockingIoCall
*/
@BlockingIoCall
public inline fun <reified T : Feature<T>> experimentBlocking(): T = runBlocking { experiment<T>() }
/**
* Returns the current value of the input [Feature].
*/
public suspend fun <T : Feature<T>> experiment(feature: Class<T>): T {
val options = feature.options
val defaultOption = getDefaultOption(feature)
val expectedName = storage.getFeatureName(defaultOption.javaClass) ?: defaultOption.name
return options.firstOrNull { it.name == expectedName } ?: defaultOption
}
/**
* Returns the current value of the input [Feature]. Warning – this call can block the calling thread.
*
* @see BlockingIoCall
*/
@BlockingIoCall
public fun <T : Feature<T>> experimentBlocking(feature: Class<T>): T = runBlocking { experiment(feature) }
/**
* Checks if a [Feature] is set to the input [option].
*/
public suspend fun <T : Feature<T>> experimentIs(option: T): Boolean = experiment(option::class.java) == option
/**
* Checks if a [Feature] is set to the input [option]. Warning – this call can block the calling thread.
*
* @see BlockingIoCall
*/
@BlockingIoCall
public fun <T : Feature<T>> experimentIsBlocking(option: T): Boolean = runBlocking { experimentIs(option) }
/**
* Sets a [Feature] to have the input [option].
*
* @return `true` if the value was set successfully, `false` otherwise.
*/
public suspend fun <T : Feature<*>> setOption(option: T): Boolean = storage.setOption(option)
@Deprecated(
message = "This method will be removed in 1.0.0. Use 'setOption()' instead.",
replaceWith = ReplaceWith("setOption(option)"),
)
public suspend fun <T : Feature<*>> setFeature(option: T): Boolean = setOption(option)
/**
* Sets a [Feature] to have the input [option]. Warning – this call can block the calling thread.
*
* @return `true` if the value was set successfully, `false` otherwise.
* @see BlockingIoCall
*/
@BlockingIoCall
public fun <T : Feature<*>> setOptionBlocking(option: T): Boolean = runBlocking { setOption(option) }
@BlockingIoCall
@Deprecated(
message = "This method will be removed in 1.0.0. Use 'setOptionBlocking()' instead.",
replaceWith = ReplaceWith("setOptionBlocking(option)"),
)
public fun <T : Feature<*>> setFeatureBlocking(option: T): Boolean = setOptionBlocking(option)
/**
* Sets [Features][Feature] to have the input [options]. If [options] contains more than one value
* for the same feature flag, the last one should be applied.
*
* @return `true` if the value was set successfully, `false` otherwise.
*/
public suspend fun <T : Feature<*>> setOptions(vararg options: T): Boolean = storage.setOptions(*options)
@Deprecated(
message = "This method will be removed in 1.0.0. Use 'setOptions()' instead.",
replaceWith = ReplaceWith("setOptions(*options)"),
)
public suspend fun <T : Feature<*>> setFeatures(vararg options: T): Boolean = setOptions(*options)
/**
* Sets [Features][Feature] to have the input [options]. If [options] contains more than one value
* for the same feature flag, the last one should be applied. Warning – this call can block the calling thread.
*
* @return `true` if the value was set successfully, `false` otherwise.
* @see BlockingIoCall
*/
@BlockingIoCall
public fun <T : Feature<*>> setOptionsBlocking(vararg options: T): Boolean = runBlocking { setOptions(*options) }
@BlockingIoCall
@Deprecated(
message = "This method will be removed in 1.0.0. Use 'setOptionsBlocking()' instead.",
replaceWith = ReplaceWith("setOptionsBlocking(*options)"),
)
public fun <T : Feature<*>> setFeaturesBlocking(vararg options: T): Boolean = setOptionsBlocking(*options)
/**
* Removes all stored feature flag options.
*
* @return `true` if the value was set successfully, `false` otherwise.
*/
public suspend fun clear(): Boolean = storage.clear()
/**
* Removes all stored feature flag options. Warning – this call can block the calling thread.
*
* @return `true` if the value was set successfully, `false` otherwise.
* @see BlockingIoCall
*/
@BlockingIoCall
public fun clearBlocking(): Boolean = runBlocking { clear() }
private fun <T : Feature<T>> getDefaultOption(
feature: Class<T>,
) = defaultOptionFactory?.create(feature) ?: feature.defaultOption
public companion object {
/**
* Creates [Laboratory] with an in-memory persistence mechanism.
*/
public fun inMemory(): Laboratory = create(FeatureStorage.inMemory())
/**
* Creates [Laboratory] with a provided [storage].
*
* @param storage [FeatureStorage] delegate that will persist all feature flags.
*/
public fun create(storage: FeatureStorage): Laboratory = builder().featureStorage(storage).build()
/**
* Creates a builder that allows to customize [Laboratory].
*/
public fun builder(): FeatureStorageStep = Builder()
}
private class Builder : FeatureStorageStep, BuildingStep {
lateinit var storage: FeatureStorage
override fun featureStorage(storage: FeatureStorage): BuildingStep = apply {
this.storage = storage
}
var defaultOptionFactory: DefaultOptionFactory? = null
override fun defaultOptionFactory(factory: DefaultOptionFactory): BuildingStep = apply {
this.defaultOptionFactory = factory
}
override fun build(): Laboratory = Laboratory(this)
}
/**
* A step of a fluent builder that requires [FeatureStorage] to proceed.
*/
public interface FeatureStorageStep {
/**
* Sets a feature storage that will be used by [Laboratory].
*/
public fun featureStorage(storage: FeatureStorage): BuildingStep
}
/**
* The final step of a fluent builder that can set optional parameters.
*/
public interface BuildingStep {
/**
* Sets a factory that can provide default options override.
*/
public fun defaultOptionFactory(factory: DefaultOptionFactory): BuildingStep
/**
* Creates a new [Laboratory] with
*/
public fun build(): Laboratory
}
}