Skip to content

Commit 3431659

Browse files
Prepare for v0.2.0 (#4)
[v0.2.0] Enabled bool and inline lambda invoke - Add boolean for enabled, removing contract extension. This is in favor of java users, since kotlin ones already have sealed-class usage + reactive one. Still, kotlin ones can use the property altough they are losing contract for type inference (not a big deal since the existence of sealed classes + reactive ones) - Add examples in all documentations - Add invoke companion operator to create lambda inlined providers - Remove both creational methods in favor of a single overloaded one
1 parent ee4cc94 commit 3431659

File tree

29 files changed

+455
-140
lines changed

29 files changed

+455
-140
lines changed

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ class RepositoryProvider(private val repository: RepositoryApi) : FeatureFlagPro
7272

7373
override fun provide(feature: FeatureFlag): FeatureFlagResult {
7474
return repository.getFeatures()[feature.key]
75-
?.let { createExistingResult(it) } // If not null, we return an existing result with it's value
76-
?: createMissingResult(feature.value) // If null we return the default value
75+
?.let { FeatureFlagResult.create(it) } // If not null, we return an existing result with it's value
76+
?: FeatureFlagResult.create(feature.value, exists = false) // If null we return the default value
7777
}
7878
}
7979
```
@@ -133,7 +133,7 @@ fun navigateHome(featureFlagProvider: FeatureFlagProvider) {
133133
fun navigateHome(featureFlagProvider: FeatureFlagProvider) {
134134
val result = featureFlagProvider.provide(FeatureCatalog.HomeV2)
135135

136-
if (result.isEnabled()) {
136+
if (result.enabled) {
137137
// Navigate to home v2
138138
} else {
139139
// Navigate to home v1
@@ -171,6 +171,28 @@ fun navigateHome(featureFlagProvider: FeatureFlagProvider) {
171171
}
172172
```
173173

174+
### Tech FAQ
175+
176+
#### Why isn't a simple interface with isFeatureEnabled and hasFeature?
177+
178+
Because we would need 2 access to the provider on any request (we want to **always** know first if
179+
it exists). There are no specifications on how the provider should retrieve a flag, hence we want
180+
to make the least possible calls per request. The feature of knowing (optionally) if a flag exists
181+
at the provider can easily be achieved with a sealed-class or a `Boolean?`.
182+
183+
On a side note, it looks more neat to just ask once for something.
184+
185+
#### Why isn't just a `Boolean` or `Boolean?` the result, instead of a sealed class
186+
187+
While seeing the requirements we saw that there were chances our servers didn't have a flag (because
188+
of developers mistakes or accessing different scopes). We wanted to know about this edge cases,
189+
but we couldn't do it if the result was a Boolean (what if `false` means it's actually `false`, and not
190+
missing?).
191+
192+
Still, using a `Boolean?` would be a pain for those who don't care about missing flags. People should
193+
always have to validate 3 cases (`null` / `false` / `true`) instead of two. Hence, a sealed class result
194+
gives all this advantages (with the only downside of having to use results instead of plain booleans)
195+
174196
### Mentions
175197

176198
The API was heavily inspired by the following contents:

docs/0.x/feature-flags/alltypes/index.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,21 @@ A Feature Toggle (aka Feature Flags) Kotlin implementation
1010

1111
##### [com.saantiaguilera.featureflags.FeatureFlag](../com.saantiaguilera.featureflags/-feature-flag/index.md)
1212

13-
data class for defining feature flags. The reason it's not an interface is because a
14-
feature flag shouldn't contain behaviours nor richer state, as it's solely reason of existence
15-
is to transfer data.
13+
Simple data class for defining feature flags.
1614

1715

1816
|
1917

2018
##### [com.saantiaguilera.featureflags.FeatureFlagProvider](../com.saantiaguilera.featureflags/-feature-flag-provider/index.md)
2119

22-
Provider of feature flags.
20+
Provides responses against requested feature flags, potentially enabling or disabling a feature.
2321

2422

2523
|
2624

2725
##### [com.saantiaguilera.featureflags.FeatureFlagResult](../com.saantiaguilera.featureflags/-feature-flag-result/index.md)
2826

29-
Result of a feature-flag fetch.
27+
Binary result of a feature-flag provision. It can be either [Enabled](../com.saantiaguilera.featureflags/-feature-flag-result/-enabled/index.md) or [Disabled](../com.saantiaguilera.featureflags/-feature-flag-result/-disabled/index.md).
3028

3129

3230
|

docs/0.x/feature-flags/com.saantiaguilera.featureflags.prioritized/-priority-feature-flag-provider/-init-.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,39 @@ Priority grouping of feature-flag providers.
99
This class will sort providers from a given comparator and one by one look for the given feature.
1010
If all results are missing types, then the default feature value (missing) will be returned.
1111

12+
Example of a basic multi-prioritized group using static integers for priorities:
13+
14+
```
15+
// Simple wrapper class for creating providers with an int priority
16+
class StaticPriorityProvider(
17+
private val provider: FeatureFlagProvider,
18+
override val priority: Int
19+
) : FeatureFlagProvider by provider, StaticPriority
20+
21+
// Contract for comparing later priorities based on integers
22+
interface StaticPriority {
23+
val priority: Int
24+
}
25+
26+
// Comparator that sorts from highest to lowest priorities
27+
class StaticPriorityComparator : Comparator<StaticPriority> {
28+
override fun compare(o1: StaticPriority?, o2: StaticPriority?): Int {
29+
val x = o1?.priority ?: 0
30+
val y = o2?.priority ?: 0
31+
return if (x > y) -1 else if (x == y) 0 else 1
32+
}
33+
}
34+
35+
// Usage:
36+
fun using() {
37+
val provider = PriorityFeatureFlagProvider(
38+
listOf(
39+
StaticPriorityProvider(yourProviderOne, priorityProviderOne),
40+
StaticPriorityProvider(yourProviderTwo, priorityProviderTwo)
41+
/* ... */
42+
),
43+
StaticPriorityComparator()
44+
}
45+
)
46+
```
47+

docs/0.x/feature-flags/com.saantiaguilera.featureflags.prioritized/-priority-feature-flag-provider/index.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@ Priority grouping of feature-flag providers.
99
This class will sort providers from a given comparator and one by one look for the given feature.
1010
If all results are missing types, then the default feature value (missing) will be returned.
1111

12+
Example of a basic multi-prioritized group using static integers for priorities:
13+
14+
```
15+
// Simple wrapper class for creating providers with an int priority
16+
class StaticPriorityProvider(
17+
private val provider: FeatureFlagProvider,
18+
override val priority: Int
19+
) : FeatureFlagProvider by provider, StaticPriority
20+
21+
// Contract for comparing later priorities based on integers
22+
interface StaticPriority {
23+
val priority: Int
24+
}
25+
26+
// Comparator that sorts from highest to lowest priorities
27+
class StaticPriorityComparator : Comparator<StaticPriority> {
28+
override fun compare(o1: StaticPriority?, o2: StaticPriority?): Int {
29+
val x = o1?.priority ?: 0
30+
val y = o2?.priority ?: 0
31+
return if (x > y) -1 else if (x == y) 0 else 1
32+
}
33+
}
34+
35+
// Usage:
36+
fun using() {
37+
val provider = PriorityFeatureFlagProvider(
38+
listOf(
39+
StaticPriorityProvider(yourProviderOne, priorityProviderOne),
40+
StaticPriorityProvider(yourProviderTwo, priorityProviderTwo)
41+
/* ... */
42+
),
43+
StaticPriorityComparator()
44+
}
45+
)
46+
```
47+
1248
### Constructors
1349

1450
| Name | Summary |

docs/0.x/feature-flags/com.saantiaguilera.featureflags/-feature-flag-provider/index.md

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,76 @@
44

55
`interface FeatureFlagProvider`
66

7-
Provider of feature flags.
7+
Provides responses against requested feature flags, potentially enabling or disabling a feature.
88

9-
There are no restrictions upon how to fetch the data, this can be an underlying cache, api-call,
10-
firebase remote config, shared-prefs, AB Test, etc. As long as it can return the state of a
11-
flag, it's ok.
9+
There are no restrictions upon how to fetch the available features, this can be an underlying
10+
cache, api-call, firebase remote config, shared-prefs, AB Test, etc. As long as it can return
11+
the state of a flag, it's ok.
12+
13+
Basic example
14+
15+
```
16+
val provider = FeatureFlagProvider { feature ->
17+
// check if the feature is enabled or not.
18+
}
19+
```
20+
21+
If a provider doesn't have the requested feature, it should respond with a missing one using
22+
[FeatureFlagResult.create](../-feature-flag-result/create.md) (specifying as a second parameter that it doesn't)
23+
24+
```
25+
val provider = FeatureFlagProvider { feature ->
26+
if (!/* check feature existence */) {
27+
// feature.value is the default provided value. We should also denote it doesn't exists.
28+
return FeatureFlagResult.create(feature.value, exists = false)
29+
}
30+
31+
// If the feature exists. return it from somewhere
32+
return FeatureFlagResult.create(/* get if the feature is enabled / disabled */)
33+
```
1234

1335
On a more complex situation, you can create multi-providers such as
1436
[com.saantiaguilera.featureflags.prioritized.PriorityFeatureFlagProvider](../../com.saantiaguilera.featureflags.prioritized/-priority-feature-flag-provider/index.md) that will, whilst
1537
abiding this contract, group a lot of providers with a given decision-rule (in this case,
1638
sorting by priority).
1739
You can also provide state into providers. Eg. If you have flags that depend on Users,
1840
because you are performing an AB Test / are bound to it, you can create a custom provider
19-
that constructs with a "User" (or know how to retrieve it) and use it.
41+
that constructs with a "User" (or know how to retrieve it) and react from it.
42+
43+
Example:
44+
45+
```
46+
class UserBasedProvider(
47+
private val userRepository: UserRepository,
48+
private val flagsRepository: MyFlagsRepository
49+
) : FeatureFlagProvider {
50+
override fun provide(feature: FeatureFlag): FeatureFlagResult {
51+
// Somewhere to get the current user from.
52+
val user = userRepository.getCurrentUser()
53+
// Somewhere to get the flags based on this user from.
54+
val flagsForUser = flagsRepository.getFlagsForUser(user)
55+
56+
// Simple lookup of a map
57+
return flagsForUser.find { it.key == feature.key }
58+
?.value?.let { FeatureFlagResult.create(it) }
59+
?: FeatureFlagResult.create(feature.value, exists = false)
60+
}
61+
}
62+
}
63+
```
2064

2165
### Functions
2266

2367
| Name | Summary |
2468
|---|---|
2569
| [provide](provide.md) | Provide for the given feature a result.`abstract fun provide(feature: `[`FeatureFlag`](../-feature-flag/index.md)`): `[`FeatureFlagResult`](../-feature-flag-result/index.md) |
2670

71+
### Companion Object Functions
72+
73+
| Name | Summary |
74+
|---|---|
75+
| [invoke](invoke.md) | Constructs a provider for a lambda. This compact syntax is most useful for inline providers.`operator fun invoke(block: (feature: `[`FeatureFlag`](../-feature-flag/index.md)`) -> `[`FeatureFlagResult`](../-feature-flag-result/index.md)`): `[`FeatureFlagProvider`](./index.md) |
76+
2777
### Inheritors
2878

2979
| Name | Summary |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[feature-flags](../../index.md) / [com.saantiaguilera.featureflags](../index.md) / [FeatureFlagProvider](index.md) / [invoke](./invoke.md)
2+
3+
# invoke
4+
5+
`operator inline fun invoke(crossinline block: (feature: `[`FeatureFlag`](../-feature-flag/index.md)`) -> `[`FeatureFlagResult`](../-feature-flag-result/index.md)`): `[`FeatureFlagProvider`](index.md)
6+
7+
Constructs a provider for a lambda. This compact syntax is most useful for inline
8+
providers.
9+
10+
```
11+
val provider = FeatureFlagProvider { feature: FeatureFlag ->
12+
// Provide a result accordingly.
13+
}
14+
```
15+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[feature-flags](../../index.md) / [com.saantiaguilera.featureflags](../index.md) / [FeatureFlagResult](index.md) / [create](./create.md)
2+
3+
# create
4+
5+
`@JvmStatic @JvmOverloads fun create(value: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)`, exists: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)` = true): `[`FeatureFlagResult`](index.md)
6+
7+
Create a result from a given value.
8+
9+
This is a shortcut to using [Enabled.Existing](-enabled/-existing.md) or [Disabled.Existing](-disabled/-existing.md) depending on the
10+
input.
11+
12+
By default, the result will be existing. You can specify as a second optional parameter
13+
if it wasn't found (hence, it was missing at the provider)
14+
This will also be a shortcut to using [Enabled.Missing](-enabled/-missing.md) or [Disabled.Missing](-disabled/-missing.md) if a
15+
second parameter is specified
16+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[feature-flags](../../index.md) / [com.saantiaguilera.featureflags](../index.md) / [FeatureFlagResult](index.md) / [enabled](./enabled.md)
2+
3+
# enabled
4+
5+
`val enabled: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)

docs/0.x/feature-flags/com.saantiaguilera.featureflags/-feature-flag-result/index.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
`sealed class FeatureFlagResult`
66

7-
Result of a feature-flag fetch.
7+
Binary result of a feature-flag provision. It can be either [Enabled](-enabled/index.md) or [Disabled](-disabled/index.md).
88

9-
The result can only be either [Enabled](-enabled/index.md) or [Disabled](-disabled/index.md).
10-
Still, both could've been found or not, which is denoted through [exists](exists.md).
9+
On advanced usages, you can also check if it was found at a provider through a depth 2 lookup of
10+
[Enabled.Existing](-enabled/-existing.md) / [Enabled.Missing](-enabled/-missing.md) or the opposing [Disabled.Existing](-disabled/-existing.md) / [Disabled.Missing](-disabled/-missing.md)
1111

1212
### Types
1313

@@ -20,13 +20,19 @@ Still, both could've been found or not, which is denoted through [exists](exists
2020

2121
| Name | Summary |
2222
|---|---|
23+
| [enabled](enabled.md) | `val enabled: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) |
2324
| [exists](exists.md) | `open val exists: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) |
2425

26+
### Companion Object Functions
27+
28+
| Name | Summary |
29+
|---|---|
30+
| [create](create.md) | Create a result from a given value.`fun create(value: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)`, exists: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)` = true): `[`FeatureFlagResult`](./index.md) |
31+
2532
### Extension Functions
2633

2734
| Name | Summary |
2835
|---|---|
29-
| [isEnabled](../is-enabled.md) | Convenience method for checking if the feature is enabled, regardless of missing / existing in the provider.`fun `[`FeatureFlagResult`](./index.md)`.isEnabled(): `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html) |
3036
| [onDisabled](../on-disabled.md) | Performs the given [action](../on-disabled.md#com.saantiaguilera.featureflags$onDisabled(com.saantiaguilera.featureflags.FeatureFlagResult, kotlin.Function1((com.saantiaguilera.featureflags.FeatureFlagResult.Disabled, kotlin.Unit)))/action) if this instance represents [disable](-disabled/index.md). Returns the original `FeatureFlagResult` unchanged.`fun `[`FeatureFlagResult`](./index.md)`.onDisabled(action: (result: Disabled) -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`FeatureFlagResult`](./index.md) |
3137
| [onEnabled](../on-enabled.md) | Performs the given [action](../on-enabled.md#com.saantiaguilera.featureflags$onEnabled(com.saantiaguilera.featureflags.FeatureFlagResult, kotlin.Function1((com.saantiaguilera.featureflags.FeatureFlagResult.Enabled, kotlin.Unit)))/action) if this instance represents [enabled](-enabled/index.md). Returns the original `FeatureFlagResult` unchanged.`fun `[`FeatureFlagResult`](./index.md)`.onEnabled(action: (result: Enabled) -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`FeatureFlagResult`](./index.md) |
3238
| [onMissing](../on-missing.md) | Performs the given [action](../on-missing.md#com.saantiaguilera.featureflags$onMissing(com.saantiaguilera.featureflags.FeatureFlagResult, kotlin.Function1((com.saantiaguilera.featureflags.FeatureFlagResult, kotlin.Unit)))/action) if this instance is a not existing queried feature. Returns the original `FeatureFlagResult` unchanged.`fun `[`FeatureFlagResult`](./index.md)`.onMissing(action: (result: `[`FeatureFlagResult`](./index.md)`) -> `[`Unit`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-unit/index.html)`): `[`FeatureFlagResult`](./index.md) |

docs/0.x/feature-flags/com.saantiaguilera.featureflags/-feature-flag/-init-.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,19 @@
44

55
`FeatureFlag(key: `[`String`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)`, value: `[`Boolean`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean/index.html)`, usage: `[`String`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/index.html)`)`
66

7-
data class for defining feature flags. The reason it's not an interface is because a
8-
feature flag shouldn't contain behaviours nor richer state, as it's solely reason of existence
9-
is to transfer data.
7+
Simple data class for defining feature flags.
108

119
This definition is based on the [flag](https://golang.org/pkg/flag) package definition.
1210

11+
Example:
12+
13+
```
14+
object PaymentsFeatureCatalog {
15+
val enableVisaPayments = FeatureFlag(
16+
"feature.payments.cards.visa",
17+
false,
18+
"Denotes the user should be able to make payments using VISA cards"
19+
)
20+
}
21+
```
22+

0 commit comments

Comments
 (0)