├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .idea
├── detekt.xml
├── dictionaries
│ └── project.xml
├── externalDependencies.xml
└── vcs.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── RELEASING.md
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── convention.detekt.gradle.kts
│ ├── convention.library.android.gradle.kts
│ ├── convention.library.kotlin.gradle.kts
│ └── convention.publishing.gradle.kts
├── config
└── detekt
│ └── detekt.yml
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── konfeature
├── build.gradle.kts
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── redmadrobot
│ │ └── konfeature
│ │ ├── FeatureConfig.kt
│ │ ├── FeatureConfigSpec.kt
│ │ ├── FeatureValue.kt
│ │ ├── FeatureValueSpec.kt
│ │ ├── Konfeature.kt
│ │ ├── Logger.kt
│ │ ├── builder
│ │ ├── KonfeatureBuilder.kt
│ │ └── KonfeatureImpl.kt
│ │ ├── exception
│ │ └── KonfeatureException.kt
│ │ └── source
│ │ ├── FeatureSource.kt
│ │ ├── FeatureValueSource.kt
│ │ ├── Interceptor.kt
│ │ └── SourceSelectionStrategy.kt
│ └── commonTest
│ └── kotlin
│ └── com
│ └── redmadrobot
│ └── konfeature
│ ├── KonfeatureTest.kt
│ ├── builder
│ └── KonfeatureBuilderTest.kt
│ └── helper
│ ├── KonfeatureTestHelper.kt
│ └── TestFeatureConfig.kt
├── release.sh
├── renovate.json
├── sample
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── com
│ └── redmadrobot
│ └── konfeature
│ └── sample
│ ├── App.kt
│ ├── FeatureToggleDebugPanelInterceptor.kt
│ ├── SampleFeatureConfig.kt
│ └── SampleSources.kt
└── settings.gradle.kts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | max_line_length = 120
11 | # Uncomment if you want to show non-strict recommended guideline
12 | #ij_visual_guides = 100
13 |
14 | # General
15 | ij_continuation_indent_size = 8
16 | ij_smart_tabs = false
17 | ij_wrap_on_typing = false
18 | ij_any_keep_indents_on_empty_lines = false
19 |
20 | # Formatter
21 | ij_formatter_tags_enabled = true
22 | ij_formatter_on_tag = @formatter:on
23 | ij_formatter_off_tag = @formatter:off
24 |
25 | [{*.kt,*.kts}]
26 | # Tabs and Indents
27 | # continuation_indent_size = 4 to match ktlint settings
28 | ij_kotlin_continuation_indent_size = 4
29 | ij_kotlin_keep_indents_on_empty_lines = unset
30 |
31 | # Spaces
32 | ## Before parentheses
33 | ij_kotlin_space_before_if_parentheses = true
34 | ij_kotlin_space_before_for_parentheses = true
35 | ij_kotlin_space_before_while_parentheses = true
36 | ij_kotlin_space_before_catch_parentheses = true
37 | ij_kotlin_space_before_when_parentheses = true
38 | ## Around operators
39 | ij_kotlin_spaces_around_assignment_operators = true
40 | ij_kotlin_spaces_around_logical_operators = true
41 | ij_kotlin_spaces_around_equality_operators = true
42 | ij_kotlin_spaces_around_relational_operators = true
43 | ij_kotlin_spaces_around_additive_operators = true
44 | ij_kotlin_spaces_around_multiplicative_operators = true
45 | ij_kotlin_spaces_around_unary_operator = false
46 | ij_kotlin_spaces_around_range = false
47 | ## Other
48 | ij_kotlin_space_before_comma = false
49 | ij_kotlin_space_after_comma = true
50 | ij_kotlin_space_before_type_colon = false
51 | ij_kotlin_space_after_type_colon = true
52 | ij_kotlin_space_before_extend_colon = true
53 | ij_kotlin_space_after_extend_colon = true
54 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
55 | ij_kotlin_spaces_around_function_type_arrow = true
56 | ij_kotlin_spaces_around_when_arrow = true
57 | ij_kotlin_space_before_lambda_arrow = true
58 |
59 | # Wrapping and Braces
60 | ## Keep when reformatting
61 | ij_kotlin_keep_line_breaks = true
62 | ij_kotlin_keep_first_column_comment = true
63 | ## Extends/implements list
64 | ij_kotlin_extends_list_wrap = normal
65 | ij_kotlin_align_multiline_extends_list = false
66 | ij_kotlin_continuation_indent_in_supertype_lists = false
67 | ## Function declaration parameters
68 | ij_kotlin_method_parameters_wrap = on_every_item
69 | ij_kotlin_align_multiline_parameters = true
70 | ij_kotlin_method_parameters_new_line_after_left_paren = true
71 | ij_kotlin_method_parameters_right_paren_on_new_line = true
72 | ij_kotlin_continuation_indent_in_parameter_lists = false
73 | ## Function call arguments
74 | ij_kotlin_call_parameters_wrap = on_every_item
75 | ij_kotlin_align_multiline_parameters_in_calls = false
76 | ij_kotlin_call_parameters_new_line_after_left_paren = true
77 | ij_kotlin_call_parameters_right_paren_on_new_line = true
78 | ij_kotlin_continuation_indent_in_argument_lists = false
79 | ## Function parentheses
80 | ij_kotlin_align_multiline_method_parentheses = false
81 | ## Chained function calls
82 | ij_kotlin_method_call_chain_wrap = normal
83 | ij_kotlin_wrap_first_method_in_call_chain = false
84 | ij_kotlin_continuation_indent_for_chained_calls = false
85 | ## 'if()' statement
86 | ij_kotlin_else_on_new_line = false
87 | ij_kotlin_if_rparen_on_new_line = true
88 | ij_kotlin_continuation_indent_in_if_conditions = false
89 | ## 'do ... while()' statement
90 | ij_kotlin_while_on_new_line = false
91 | ## 'try' statement
92 | ij_kotlin_catch_on_new_line = false
93 | ij_kotlin_finally_on_new_line = false
94 | ## Binary expressions
95 | ij_kotlin_align_multiline_binary_operation = false
96 | ## Wraps
97 | ij_kotlin_assignment_wrap = normal
98 | ij_kotlin_enum_constants_wrap = off
99 | ij_kotlin_class_annotation_wrap = split_into_lines
100 | ij_kotlin_method_annotation_wrap = split_into_lines
101 | ij_kotlin_field_annotation_wrap = split_into_lines
102 | ij_kotlin_parameter_annotation_wrap = off
103 | ij_kotlin_variable_annotation_wrap = off
104 | ## 'when' statements
105 | ij_kotlin_align_in_columns_case_branch = false
106 | ij_kotlin_line_break_after_multiline_when_entry = true
107 | ## Braces placement
108 | ij_kotlin_lbrace_on_next_line = false
109 | ## Expression body functions
110 | ij_kotlin_wrap_expression_body_functions = 1
111 | ij_kotlin_continuation_indent_for_expression_bodies = false
112 | ## Elvis expressions
113 | ij_kotlin_wrap_elvis_expressions = 1
114 | ij_kotlin_continuation_indent_in_elvis = false
115 |
116 | # Blank Lines
117 | ## Keep maximum blank lines
118 | ij_kotlin_keep_blank_lines_in_declarations = 1
119 | ij_kotlin_keep_blank_lines_in_code = 1
120 | ij_kotlin_keep_blank_lines_before_right_brace = 0
121 | ## Minimum blank lines
122 | ij_kotlin_blank_lines_after_class_header = 0
123 | ij_kotlin_blank_lines_around_block_when_branches = 1
124 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
125 |
126 | # Imports
127 | ij_kotlin_name_count_to_use_star_import = 5
128 | ij_kotlin_name_count_to_use_star_import_for_members = 3
129 | ij_kotlin_import_nested_classes = false
130 | ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
131 | ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
132 |
133 | # Other
134 | ## Trailing comma
135 | ij_kotlin_allow_trailing_comma = true
136 | ij_kotlin_allow_trailing_comma_on_call_site = false
137 |
138 | # Code generation
139 | ## Comment code
140 | ij_kotlin_line_comment_at_first_column = true
141 | ij_kotlin_line_comment_add_space = false
142 | ij_kotlin_line_comment_add_space_on_reformat = false
143 | ij_kotlin_block_comment_at_first_column = true
144 | ij_kotlin_block_comment_add_space = false
145 |
146 | # Compose
147 | ij_kotlin_use_custom_formatting_for_modifiers = true
148 |
149 | # Load/Save
150 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
151 |
152 | [*.kts]
153 | # Always use wildcard imports in scripts
154 | ij_kotlin_name_count_to_use_star_import = 2
155 |
156 | # EditorConfig can not set some of XML code style options.
157 | # Remember to set default Android XML code style: Editor > Code Style > XML > Set from... -> Android
158 | [{**/res/**.xml,**/AndroidManifest.xml}]
159 | # Tabs and Indents
160 | ij_xml_continuation_indent_size = 4
161 | ij_xml_keep_indents_on_empty_lines = unset
162 |
163 | # Other
164 | ij_xml_keep_line_breaks = false
165 | ij_xml_keep_line_breaks_in_text = true
166 | ij_xml_keep_blank_lines = 2
167 | ij_xml_attribute_wrap = normal
168 | ij_xml_text_wrap = normal
169 | ij_xml_align_text = false
170 | ij_xml_align_attributes = false
171 | ij_xml_keep_whitespaces = false
172 | ## Spaces
173 | ij_xml_space_around_equals_in_attribute = false
174 | ij_xml_space_after_tag_name = false
175 | ij_xml_space_inside_empty_tag = true
176 | ## CDATA
177 | ij_xml_keep_whitespaces_around_cdata = preserve
178 | ij_xml_keep_whitespaces_inside_cdata = false
179 |
180 | # Code Generation
181 | ij_xml_line_comment_at_first_column = true
182 | ij_xml_block_comment_at_first_column = true
183 | ij_xml_block_comment_add_space = false
184 |
185 | # Android
186 | ij_xml_use_custom_settings = true
187 |
188 | [*.md]
189 | trim_trailing_whitespace = false
190 |
191 | # Wrapping and Braces
192 | ij_markdown_wrap_text_if_long = false
193 | ij_markdown_wrap_text_inside_blockquotes = false
194 | ## When reformatting
195 | ij_markdown_keep_line_breaks_inside_text_blocks = true
196 | ij_markdown_insert_quote_arrows_on_wrap = true
197 | ij_markdown_format_tables = true
198 |
199 | # Tabs and Indents
200 | ij_markdown_keep_indents_on_empty_lines = unset
201 |
202 | # Blank Lines
203 | ## Keep maximum blank lines
204 | ij_markdown_max_lines_around_header = 1
205 | ij_markdown_max_lines_around_block_elements = 1
206 | ij_markdown_max_lines_between_paragraphs = 1
207 | ## Minimum blank lines
208 | ij_markdown_min_lines_around_header = 1
209 | ij_markdown_min_lines_around_block_elements = 1
210 | ij_markdown_min_lines_between_paragraphs = 1
211 |
212 | # Spaces
213 | ## Force one space
214 | ij_markdown_force_one_space_between_words = true
215 | ij_markdown_force_one_space_after_header_symbol = true
216 | ij_markdown_force_one_space_after_list_bullet = true
217 | ij_markdown_force_one_space_after_blockquote_symbol = true
218 |
219 | [{*.yaml,*.yml}]
220 | indent_size = 2
221 | ij_yaml_keep_indents_on_empty_lines = unset
222 | ij_yaml_keep_line_breaks = true
223 | ij_yaml_spaces_within_brackets = false
224 |
225 | [{*.bash,*.sh,*.zsh}]
226 | indent_size = 2
227 | tab_width = 2
228 |
229 | [*.bat]
230 | end_of_line = crlf
231 |
232 | [*.properties]
233 | ij_properties_keep_blank_lines = true
234 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # These are explicitly windows files and should use crlf
5 | *.bat text eol=crlf
6 |
7 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | # Release tag format is v[version]
7 | # For example: v1.3.5
8 | tags: ["v*"]
9 | pull_request:
10 | branches: [main]
11 |
12 | jobs:
13 | check:
14 | name: Check
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout sources
18 | uses: actions/checkout@v4
19 | - name: Setup Java
20 | uses: actions/setup-java@v4
21 | with:
22 | distribution: "temurin"
23 | java-version: 17
24 | - name: Setup Gradle
25 | uses: gradle/actions/setup-gradle@v3
26 | - name: Run Check
27 | run: ./gradlew check detektAll
28 |
29 | publish:
30 | name: Publish
31 | needs: check
32 | runs-on: ubuntu-latest
33 | if: ${{ startsWith(github.ref, 'refs/tags/') }}
34 |
35 | steps:
36 | - name: Checkout sources
37 | uses: actions/checkout@v4
38 | - name: Setup Java
39 | uses: actions/setup-java@v4
40 | with:
41 | distribution: "temurin"
42 | java-version: 17
43 | - name: Setup Gradle
44 | uses: gradle/actions/setup-gradle@v3
45 | - name: Run Publish
46 | run: ./gradlew publish
47 | env:
48 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
49 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
50 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
51 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
52 | ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
53 | ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
54 | - name: Extract release notes
55 | uses: ffurrer2/extract-release-notes@v2
56 | with:
57 | release_notes_file: RELEASE_NOTES.md
58 | - name: Create GitHub Release
59 | uses: softprops/action-gh-release@v2
60 | with:
61 | body_path: RELEASE_NOTES.md
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | ## JetBrains IDEs
4 | /.idea/**
5 | *.iml
6 |
7 | # Keep required plugins
8 | !.idea/externalDependencies.xml
9 |
10 | # Keep detekt plugin config
11 | !.idea/detekt.xml
12 |
13 | # Keep VCS config
14 | !.idea/vcs.xml
15 |
16 | # Keep project dictionary
17 | !.idea/dictionaries
18 | !.idea/dictionaries/project.xml
19 |
20 | ## Gradle
21 | # Ignore Gradle project-specific cache directory
22 | .gradle
23 |
24 | # Ignore Gradle build output directory
25 | **/build/
26 | !**/src/**/build/
27 |
28 | # Ignore local properties
29 | local.properties
30 |
--------------------------------------------------------------------------------
/.idea/detekt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/dictionaries/project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | konfeature
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/externalDependencies.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [Unreleased]
2 |
3 | ### Changed
4 |
5 | - no changes
6 |
7 | ## v0.1.0 (2024-07-25)
8 |
9 | Initial public release
10 |
11 | [unreleased]: https://github.com/RedMadRobot/konfeature/compare/v0.1.0...main
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 red_mad_robot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Konfeature
2 |
3 | [][mavenCentral]
4 | [][ci]
5 | [][license]
6 |
7 |
8 | Working with remote configuration has become a standard part of the development process for almost any application. Depending on the complexity of the application, several requirements for such functionality may arise, including:
9 | - convenient syntax for declaring configuration elements
10 | - the ability to separate configuration into different files for different features
11 | - the ability to make the configuration local-only during active feature development
12 | - support for multiple data sources for remote config
13 | - the ability to view a list of all configurations and modify their values for debugging purposes
14 | - logging the value and its source when accessing the configuration, as well as logging non-critical errors
15 |
16 | We have made every effort to meet all these requirements in the development of Konfeature.
17 |
18 | ---
19 |
20 |
21 |
22 | - [Installation](#installation)
23 | - [Usage](#usage)
24 | - [FeatureConfig](#featureconfig)
25 | - [FeatureSource](#featuresource)
26 | - [SourceSelectionStrategy](#sourceselectionstrategy)
27 | - [Interceptor](#interceptor)
28 | - [Logger](#logger)
29 | - [Spec](#spec)
30 | - [Ordering](#ordering)
31 | - [Contributing](#contributing)
32 |
33 |
34 |
35 | ## Installation
36 |
37 | Add the dependency:
38 |
39 | ```groovy
40 | repositories {
41 | mavenCentral()
42 | }
43 |
44 | dependencies {
45 | implementation("com.redmadrobot.konfeature:konfeature:")
46 | }
47 | ```
48 |
49 | ## Usage
50 |
51 | ### FeatureConfig
52 |
53 | Defines a set of configuration elements, where each element is defined using a delegate.
54 | There are two types of delegates:
55 | - `by toggle(...)` - used for elements of type `Boolean`
56 | - `by value(...)` - used for elements of any other type
57 |
58 | ```kotlin
59 | class ProfileFeatureConfig : FeatureConfig(
60 | name = "profile_feature_config",
61 | description = "Config of features for profile usage"
62 | ) {
63 | val isProfileFeatureEnabled: Boolean by toggle(
64 | key = "profile_feature",
65 | description = "show profile entry point for user",
66 | defaultValue = false,
67 | )
68 |
69 | val profileFeatureTitle: String by value(
70 | key = "profile_feature_title",
71 | description = "title of profile entry point button",
72 | defaultValue = "Feature number nine",
73 | sourceSelectionStrategy = SourceSelectionStrategy.Any
74 | )
75 |
76 | val profileButtonAppearDuration: Long by value(
77 | key = "profile_button_appear_duration",
78 | description = "duration of profile button appearing in ms",
79 | defaultValue = 200,
80 | sourceSelectionStrategy = SourceSelectionStrategy.Any
81 | )
82 | }
83 | ```
84 |
85 | The configuration requires specifying:
86 | - `name` - the name of the configuration
87 | - `description` - a detailed description of the configuration
88 |
89 | Each configuration element requires specifying:
90 | - `key` - used to retrieve the value of the element from a `Source`
91 | - `description` - a detailed description of the element
92 | - `defaultValue` - used if the value cannot be found in a `Source`
93 | - `sourceSelectionStrategy` - the strategy for selecting a `Source` using [SourceSelectionStrategy](#sourceselectionstrategy)
94 |
95 | After that, you need to register the configuration in `Konfeature`:
96 |
97 | ```kotlin
98 | val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig()
99 |
100 | val konfeatureInstance = konfeature {
101 | register(profileFeatureConfig)
102 | }
103 | ```
104 |
105 | >Similarly, you can add multiple configurations, for example, for each module, when organizing multi-modularity by features.
106 |
107 | ### FeatureSource
108 |
109 | An abstraction over the value source for configuration elements.
110 |
111 | ```kotlin
112 | public interface FeatureSource {
113 |
114 | public val name: String
115 |
116 | public fun get(key: String): Any?
117 | }
118 | ```
119 | - `name` - source name
120 | - `get(key: String)` - logic for getting values by `key`
121 |
122 | Example implementation based on `FirebaseRemoteConfig`:
123 |
124 | ```kotlin
125 | class FirebaseFeatureSource(
126 | private val remoteConfig: FirebaseRemoteConfig
127 | ) : FeatureSource {
128 |
129 | override val name: String = "FirebaseRemoteConfig"
130 |
131 | override fun get(key: String): Any? {
132 | return remoteConfig
133 | .getValue(key)
134 | .takeIf { source == FirebaseRemoteConfig.VALUE_SOURCE_REMOTE }
135 | ?.let { value: FirebaseRemoteConfigValue ->
136 | value.getOrNull { asBoolean() }
137 | ?: value.getOrNull { asString() }
138 | ?: value.getOrNull { asLong() }
139 | ?: value.getOrNull { asDouble() }
140 | }
141 | }
142 |
143 | private fun FirebaseRemoteConfigValue.getOrNull(
144 | getter: FirebaseRemoteConfigValue.() -> Any?
145 | ): Any? {
146 | return try {
147 | getter()
148 | } catch (error: IllegalArgumentException) {
149 | null
150 | }
151 | }
152 | }
153 | ```
154 | After that, you need to add the `Source` in `Konfeature`:
155 |
156 | ```kotlin
157 | val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig()
158 | val source: FeatureSource = FirebaseFeatureSource(remoteConfig)
159 |
160 | val konfeatureInstance = konfeature {
161 | addSource(source)
162 | register(profileFeatureConfig)
163 | }
164 | ```
165 |
166 | >Similarly, you can add multiple sources, for example, Huawei AppGallery, RuStore, or your own backend.
167 |
168 | ### SourceSelectionStrategy
169 |
170 | You can configure the retrieval of an element's value from the source more flexibly by using the `sourceSelectionStrategy` parameter:
171 |
172 | ```kotlin
173 | val profileFeatureTitle: String by value(
174 | key = "profile_feature_title",
175 | description = "title of profile entry point button",
176 | defaultValue = "Feature number nine",
177 | sourceSelectionStrategy = SourceSelectionStrategy.Any
178 | )
179 | ```
180 |
181 | Where `sourceSelectionStrategy` filters the available data sources.
182 |
183 | ```kotlin
184 | public fun interface SourceSelectionStrategy {
185 |
186 | public fun select(names: Set): Set
187 |
188 | public companion object {
189 | public val None: SourceSelectionStrategy = SourceSelectionStrategy { emptySet() }
190 | public val Any: SourceSelectionStrategy = SourceSelectionStrategy { it }
191 |
192 | public fun anyOf(vararg sources: String): SourceSelectionStrategy = SourceSelectionStrategy { sources.toSet() }
193 | }
194 | }
195 | ```
196 |
197 | The `select(...)` method receives a list of available `Source` names and returns a list of sources from which the configuration element can retrieve a value.
198 |
199 | For most scenarios, predefined implementations will be sufficient:
200 | - `SourceSelectionStrategy.None` - prohibits taking values from any source, i.e., the value specified in `defaultValue` will always be used
201 | - `SourceSelectionStrategy.Any` - allows taking values from any source
202 | - `SourceSelectionStrategy.anyOf("Source 1", ... ,"Source N")` - allows taking values from the specified list of sources
203 |
204 | > [!IMPORTANT]
205 | > By default, `SourceSelectionStrategy.None` is used!
206 |
207 | ### Interceptor
208 |
209 | Allows intercepting and overriding the value of the element.
210 |
211 | ```kotlin
212 | public interface Interceptor {
213 |
214 | public val name: String
215 |
216 | public fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any?
217 | }
218 | ```
219 |
220 | - `name` - the name of the interceptor
221 | - `intercept(valueSource: FeatureValueSource, key: String, value: Any): Any?` - called when accessing the element with `key` and `value` from `valueSource(Source(), Interceptor(), Default)`, and returns its new value or `null` if it doesn't change
222 |
223 | Example of implementation based on `DebugPanelInterceptor`:
224 |
225 | ```kotlin
226 | class DebugPanelInterceptor : Interceptor {
227 |
228 | private val values = mutableMapOf()
229 |
230 | override val name: String = "DebugPanelInterceptor"
231 |
232 | override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? {
233 | return values[key]
234 | }
235 |
236 | fun setFeatureValue(key: String, value: Any) {
237 | values[key] = value
238 | }
239 |
240 | fun removeFeatureValue(key: String) {
241 | values.remove(key)
242 | }
243 | }
244 | ```
245 |
246 | After that, you need to add the `Interceptor` in `Konfeature`:
247 |
248 | ```kotlin
249 | val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig()
250 | val source: FeatureSource = FirebaseFeatureSource(remoteConfig)
251 | val debugPanelInterceptor: Interceptor = DebugPanelInterceptor()
252 |
253 | val konfeatureInstance = konfeature {
254 | addSource(source)
255 | register(profileFeatureConfig)
256 | addInterceptor(debugPanelInterceptor)
257 | }
258 | ```
259 |
260 | >Similarly, you can add multiple interceptors.
261 |
262 | ### Logger
263 |
264 | ```kotlin
265 | public interface Logger {
266 |
267 | public fun log(severity: Severity, message: String)
268 |
269 | public enum class Severity {
270 | WARNING, INFO
271 | }
272 | }
273 | ```
274 |
275 | The following events are logged:
276 |
277 | - key, value, and its source when requested
278 | >Get value 'true' by key 'profile_feature' from 'Source(name=FirebaseRemoteConfig)'
279 | - `Source` or `Interceptor` returns an unexpected type for `key`
280 | >Unexpected value type for 'profile_button_appear_duration': expected type is 'kotlin.Long', but value from 'Source(name=FirebaseRemoteConfig)' is 'true' with type 'kotlin.Boolean'
281 |
282 | Example of implementation based on `Timber`:
283 |
284 | ```kotlin
285 | class TimberLogger: Logger {
286 |
287 | override fun log(severity: Severity, message: String) {
288 | if (severity == INFO) {
289 | Timber.tag(TAG).i(message)
290 | } else if (severity == WARNING) {
291 | Timber.tag(TAG).w(message)
292 | }
293 | }
294 |
295 | companion object {
296 | private const val TAG = "Konfeature"
297 | }
298 | }
299 | ```
300 |
301 | After that, you need to add the `Logger` in `Konfeature`:
302 |
303 | ```kotlin
304 | val profileFeatureConfig: FeatureConfig = ProfileFeatureConfig()
305 | val source: FeatureSource = FirebaseFeatureSource(remoteConfig)
306 | val debugPanelInterceptor: Interceptor = DebugPanelInterceptor()
307 | val logger: Logger = TimberLogger()
308 |
309 | val konfeatureInstance = konfeature {
310 | addSource(source)
311 | register(profileFeatureConfig)
312 | addInterceptor(debugPanelInterceptor)
313 | setLogger(logger)
314 | }
315 | ```
316 |
317 | ### Spec
318 |
319 | Konfeature contains information about all registered `FeatureConfig` in the form of `spec`:
320 |
321 | ```kotlin
322 | public interface Konfeature {
323 |
324 | public val spec: List
325 |
326 | public fun getValue(spec: FeatureValueSpec): FeatureValue
327 | }
328 | ```
329 |
330 | This allows you to obtain information about added configurations as well as the current value of each element:
331 |
332 | ```kotlin
333 | val konfeatureInstance = konfeature {...}
334 |
335 | val featureConfigSpec = konfeatureInstance.spec[0]
336 | val featureSpec = featureConfigSpec.values[0]
337 | val featureValue = konfeatureInstance.getValue(featureSpec)
338 | ```
339 | > This can be useful for use in the DebugPanel
340 |
341 | ## Ordering
342 | The value of the configuration element is determined in the following order:
343 |
344 | - `defaultValue` and `Default` source are assigned.
345 | - Using `sourceSelectionStrategy`, a list of `Sources` from which a value can be requested is determined.
346 | - Search the list of `Sources` in the order they were added to `Konfeature`, **stopping at the first occurrence** of the element by `key`.
347 | Upon successful search, the value from `Source` is assigned with `Source(name=SourceName)` source.
348 | - Search the list of `Interceptors` in the order they were added to `Konfeature`.
349 | If `Interceptor` returns a value other than `null`, this value is assigned with `Interceptor(name=InterceptorName)` source.
350 |
351 | ## Contributing
352 |
353 | Merge requests are welcome.
354 | For major changes, please open an issue first to discuss what you would like to change.
355 |
356 | [mavenCentral]: https://central.sonatype.com/artifact/com.redmadrobot.konfeature/konfeature
357 | [ci]: https://github.com/RedMadRobot/Konfeature/actions?query=branch%3Amain
358 | [license]: ./LICENSE
359 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | 1. Run the script `release.sh` with the desired version as a parameter:
4 | ```bash
5 | ./release.sh [version]
6 | ````
7 |
8 | 2. The script will ask you if you want to push the release branch and create a release tag.
9 |
10 | 3. Ensure `CHANGELOG.md` looks good and is ready to be published.
11 |
12 | 4. Type "yes" to the console if everything is okay.
13 | Tag push triggers a GitHub Actions workflow,
14 | which publishes the release artifacts to Maven Central and creates a GitHub release.
15 |
16 | 5. Click the link displayed in the console to create a Pull Request for release branch.
17 |
18 | 6. Merge the Pull Request as soon as the "Check" workflow succeeds.
19 | It is recommended to use fast-forward merge to merge release branches.
20 |
21 | ## Manual release preparation
22 |
23 | To prepare a release manually, follow the steps the script does:
24 |
25 | 1. Ensure the repository is up to date, and the main branch is checked out.
26 |
27 | 2. Create the release branch with the name `release/[version]`.
28 |
29 | 3. Update the version in `gradle.properties` and `README.md` ("Usage" section) with the version to be released.
30 |
31 | 4. Update the `CHANGELOG.md`:
32 | 1. Replace `Unreleased` section with the release version
33 | 2. Add a link to the diff between the previous and the new version
34 | 3. Add a new empty `Unreleased` section on the top
35 |
36 | 5. Commit the changes, create a tag on the latest commit, and push it to the remote repository.
37 | The tag should follow the format `v[version]`.
38 |
39 | 6. Create a Pull Request for the release branch and merge it.
40 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.infrastructure.detekt)
3 | alias(libs.plugins.versions)
4 | convention.detekt
5 | }
6 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | tasks.withType {
6 | kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
7 | }
8 |
9 | java {
10 | targetCompatibility = JavaVersion.VERSION_11
11 | sourceCompatibility = JavaVersion.VERSION_11
12 | }
13 |
14 | dependencies {
15 | implementation(libs.infrastructure.publish)
16 | implementation(libs.infrastructure.android)
17 | implementation(libs.publish.gradlePlugin)
18 | implementation(libs.gradle.android.cacheFixGradlePlugin)
19 | implementation(libs.kotlin.gradlePlugin)
20 | implementation(libs.detekt.gradlePlugin)
21 | implementation(libs.android.gradlePlugin)
22 | }
23 |
--------------------------------------------------------------------------------
/buildSrc/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google {
5 | content {
6 | includeGroupAndSubgroups("com.android")
7 | includeGroupAndSubgroups("com.google")
8 | includeGroupAndSubgroups("androidx")
9 | }
10 | }
11 | mavenCentral()
12 | }
13 | }
14 |
15 | @Suppress("UnstableApiUsage")
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 |
19 | repositories {
20 | google {
21 | content {
22 | includeGroupAndSubgroups("com.android")
23 | includeGroupAndSubgroups("com.google")
24 | includeGroupAndSubgroups("androidx")
25 | }
26 | }
27 |
28 | mavenCentral()
29 | gradlePluginPortal()
30 | }
31 |
32 | versionCatalogs {
33 | create("libs") {
34 | from(files("../gradle/libs.versions.toml"))
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/convention.detekt.gradle.kts:
--------------------------------------------------------------------------------
1 | import io.gitlab.arturbosch.detekt.*
2 |
3 | plugins {
4 | id("io.gitlab.arturbosch.detekt")
5 | }
6 |
7 | tasks.withType().configureEach {
8 | jvmTarget = JavaVersion.VERSION_11.toString()
9 | }
10 | tasks.withType().configureEach {
11 | jvmTarget = JavaVersion.VERSION_11.toString()
12 | }
13 |
14 | dependencies {
15 | //noinspection UseTomlInstead
16 | detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.6")
17 | //noinspection UseTomlInstead
18 | detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6")
19 | }
20 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/convention.library.android.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.redmadrobot.android-library")
3 | id("convention.publishing")
4 | id("convention.detekt")
5 | }
6 |
7 | redmadrobot {
8 | android.minSdk = 21
9 | }
10 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/convention.library.kotlin.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.redmadrobot.kotlin-library")
3 | id("convention.publishing")
4 | id("convention.detekt")
5 | }
6 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/convention.publishing.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.redmadrobot.build.dsl.*
2 | import com.vanniktech.maven.publish.SonatypeHost
3 |
4 | plugins {
5 | id("com.vanniktech.maven.publish")
6 | }
7 |
8 | mavenPublishing {
9 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
10 | signAllPublications()
11 |
12 | pom {
13 | name.convention(project.name)
14 | description.convention(project.description)
15 |
16 | licenses {
17 | mit()
18 | }
19 |
20 | developers {
21 | developer(id = "AleksandrTabolin", name = "Aleksandr Tabolin", email = "a.tabolin@redmadrobot.com")
22 | }
23 |
24 | setGitHubProject("RedMadRobot/Konfeature")
25 | }
26 | }
27 |
28 | publishing {
29 | repositories {
30 | if (isRunningOnCi) githubPackages("RedMadRobot/Konfeature")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/config/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | complexity: 2
6 | style: 1
7 | comments: 1
8 |
9 | config:
10 | validation: true
11 | warningsAsErrors: true
12 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
13 | excludes: ''
14 |
15 | processors:
16 | active: true
17 | exclude:
18 | - 'DetektProgressListener'
19 | # - 'KtFileCountProcessor'
20 | # - 'PackageCountProcessor'
21 | # - 'ClassCountProcessor'
22 | # - 'FunctionCountProcessor'
23 | # - 'PropertyCountProcessor'
24 | # - 'ProjectComplexityProcessor'
25 | # - 'ProjectCognitiveComplexityProcessor'
26 | # - 'ProjectLLOCProcessor'
27 | # - 'ProjectCLOCProcessor'
28 | # - 'ProjectLOCProcessor'
29 | # - 'ProjectSLOCProcessor'
30 | # - 'LicenseHeaderLoaderExtension'
31 |
32 | console-reports:
33 | active: true
34 | exclude:
35 | - 'ProjectStatisticsReport'
36 | - 'ComplexityReport'
37 | - 'NotificationReport'
38 | # - 'FindingsReport'
39 | - 'FileBasedFindingsReport'
40 | - 'LiteFindingsReport'
41 |
42 | output-reports:
43 | active: true
44 | exclude: []
45 | # - 'TxtOutputReport'
46 | # - 'XmlOutputReport'
47 | # - 'HtmlOutputReport'
48 |
49 | comments:
50 | active: true
51 | AbsentOrWrongFileLicense:
52 | active: false
53 | licenseTemplateFile: 'license.template'
54 | licenseTemplateIsRegex: false
55 | CommentOverPrivateFunction:
56 | active: false
57 | CommentOverPrivateProperty:
58 | active: false
59 | DeprecatedBlockTag:
60 | active: false
61 | EndOfSentenceFormat:
62 | active: false
63 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
64 | OutdatedDocumentation:
65 | active: false
66 | matchTypeParameters: true
67 | matchDeclarationsOrder: true
68 | UndocumentedPublicClass:
69 | active: false
70 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
71 | searchInNestedClass: true
72 | searchInInnerClass: true
73 | searchInInnerObject: true
74 | searchInInnerInterface: true
75 | UndocumentedPublicFunction:
76 | active: false
77 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
78 | UndocumentedPublicProperty:
79 | active: false
80 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
81 |
82 | complexity:
83 | active: true
84 | ComplexCondition:
85 | active: true
86 | threshold: 4
87 | ComplexInterface:
88 | active: false
89 | threshold: 10
90 | includeStaticDeclarations: false
91 | includePrivateDeclarations: false
92 | CognitiveComplexMethod:
93 | active: true
94 | threshold: 15
95 | LabeledExpression:
96 | active: false
97 | ignoredLabels: []
98 | LargeClass:
99 | active: true
100 | threshold: 600
101 | LongMethod:
102 | active: true
103 | threshold: 60
104 | LongParameterList:
105 | active: true
106 | functionThreshold: 6
107 | constructorThreshold: 7
108 | ignoreDefaultParameters: true
109 | ignoreDataClasses: true
110 | ignoreAnnotatedParameter: []
111 | MethodOverloading:
112 | active: false
113 | threshold: 6
114 | NamedArguments:
115 | active: true
116 | threshold: 3
117 | NestedBlockDepth:
118 | active: true
119 | threshold: 4
120 | ReplaceSafeCallChainWithRun:
121 | active: true
122 | StringLiteralDuplication:
123 | active: true
124 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
125 | threshold: 3
126 | ignoreAnnotation: true
127 | excludeStringsWithLessThan5Characters: true
128 | ignoreStringsRegex: '^(boolean|false)$'
129 | TooManyFunctions:
130 | active: true
131 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
132 | thresholdInFiles: 99
133 | thresholdInClasses: 11
134 | thresholdInInterfaces: 11
135 | thresholdInObjects: 11
136 | thresholdInEnums: 11
137 | ignoreDeprecated: false
138 | ignorePrivate: true
139 | ignoreOverridden: true
140 |
141 | coroutines:
142 | active: true
143 | GlobalCoroutineUsage:
144 | active: true
145 | InjectDispatcher:
146 | active: true
147 | dispatcherNames:
148 | - 'IO'
149 | - 'Default'
150 | - 'Unconfined'
151 | RedundantSuspendModifier:
152 | active: true
153 | SleepInsteadOfDelay:
154 | active: true
155 | SuspendFunWithFlowReturnType:
156 | active: true
157 |
158 | empty-blocks:
159 | active: true
160 | EmptyCatchBlock:
161 | active: true
162 | allowedExceptionNameRegex: '_|(ignore|expected).*'
163 | EmptyClassBlock:
164 | active: true
165 | EmptyDefaultConstructor:
166 | active: true
167 | EmptyDoWhileBlock:
168 | active: true
169 | EmptyElseBlock:
170 | active: true
171 | EmptyFinallyBlock:
172 | active: true
173 | EmptyForBlock:
174 | active: true
175 | EmptyFunctionBlock:
176 | active: true
177 | ignoreOverridden: false
178 | EmptyIfBlock:
179 | active: true
180 | EmptyInitBlock:
181 | active: true
182 | EmptyKtFile:
183 | active: true
184 | EmptySecondaryConstructor:
185 | active: true
186 | EmptyTryBlock:
187 | active: true
188 | EmptyWhenBlock:
189 | active: true
190 | EmptyWhileBlock:
191 | active: true
192 |
193 | exceptions:
194 | active: true
195 | ExceptionRaisedInUnexpectedLocation:
196 | active: true
197 | methodNames:
198 | - 'equals'
199 | - 'finalize'
200 | - 'hashCode'
201 | - 'toString'
202 | InstanceOfCheckForException:
203 | active: true
204 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
205 | NotImplementedDeclaration:
206 | active: true
207 | ObjectExtendsThrowable:
208 | active: true
209 | PrintStackTrace:
210 | active: true
211 | RethrowCaughtException:
212 | active: true
213 | ReturnFromFinally:
214 | active: true
215 | ignoreLabeled: false
216 | SwallowedException:
217 | active: true
218 | ignoredExceptionTypes:
219 | - 'InterruptedException'
220 | - 'MalformedURLException'
221 | - 'NumberFormatException'
222 | - 'ParseException'
223 | allowedExceptionNameRegex: '_|(ignore|expected).*'
224 | ThrowingExceptionFromFinally:
225 | active: true
226 | ThrowingExceptionInMain:
227 | active: false
228 | ThrowingExceptionsWithoutMessageOrCause:
229 | active: true
230 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
231 | exceptions:
232 | - 'ArrayIndexOutOfBoundsException'
233 | - 'Exception'
234 | - 'IllegalArgumentException'
235 | - 'IllegalMonitorStateException'
236 | - 'IllegalStateException'
237 | - 'IndexOutOfBoundsException'
238 | - 'NullPointerException'
239 | - 'RuntimeException'
240 | - 'Throwable'
241 | ThrowingNewInstanceOfSameException:
242 | active: true
243 | TooGenericExceptionCaught:
244 | active: true
245 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
246 | exceptionNames:
247 | - 'ArrayIndexOutOfBoundsException'
248 | - 'Error'
249 | - 'Exception'
250 | - 'IllegalMonitorStateException'
251 | - 'IndexOutOfBoundsException'
252 | - 'NullPointerException'
253 | - 'RuntimeException'
254 | - 'Throwable'
255 | allowedExceptionNameRegex: '_|(ignore|expected).*'
256 | TooGenericExceptionThrown:
257 | active: true
258 | exceptionNames:
259 | - 'Error'
260 | - 'Exception'
261 | - 'RuntimeException'
262 | - 'Throwable'
263 |
264 | formatting:
265 | active: true
266 | android: true
267 | autoCorrect: true
268 | AnnotationOnSeparateLine:
269 | active: false
270 | autoCorrect: true
271 | AnnotationSpacing:
272 | active: true
273 | autoCorrect: true
274 | ArgumentListWrapping:
275 | # TODO: Enable after update to ktlint 0.42+
276 | # Issue: https://github.com/pinterest/ktlint/issues/1159
277 | active: false
278 | autoCorrect: true
279 | indentSize: 4
280 | maxLineLength: 120
281 | ChainWrapping:
282 | active: true
283 | autoCorrect: true
284 | CommentSpacing:
285 | active: true
286 | autoCorrect: true
287 | EnumEntryNameCase:
288 | active: false
289 | autoCorrect: true
290 | Filename:
291 | active: true
292 | FinalNewline:
293 | active: true
294 | autoCorrect: true
295 | insertFinalNewLine: true
296 | ImportOrdering:
297 | active: true
298 | autoCorrect: true
299 | layout: '*,java.**,javax.**,kotlin.**,^'
300 | Indentation:
301 | active: true
302 | autoCorrect: true
303 | indentSize: 4
304 | MaximumLineLength:
305 | active: false # See style.MaxLineLength
306 | maxLineLength: 120
307 | ignoreBackTickedIdentifier: true
308 | ModifierOrdering:
309 | active: true
310 | autoCorrect: true
311 | MultiLineIfElse:
312 | active: true
313 | autoCorrect: true
314 | NoBlankLineBeforeRbrace:
315 | active: true
316 | autoCorrect: true
317 | NoConsecutiveBlankLines:
318 | active: true
319 | autoCorrect: true
320 | NoEmptyClassBody:
321 | active: true
322 | autoCorrect: true
323 | NoEmptyFirstLineInMethodBlock:
324 | active: true
325 | autoCorrect: true
326 | NoLineBreakAfterElse:
327 | active: true
328 | autoCorrect: true
329 | NoLineBreakBeforeAssignment:
330 | active: true
331 | autoCorrect: true
332 | NoMultipleSpaces:
333 | active: true
334 | autoCorrect: true
335 | NoSemicolons:
336 | active: true
337 | autoCorrect: true
338 | NoTrailingSpaces:
339 | active: true
340 | autoCorrect: true
341 | NoUnitReturn:
342 | active: true
343 | autoCorrect: true
344 | NoUnusedImports:
345 | active: true
346 | autoCorrect: true
347 | NoWildcardImports:
348 | active: false
349 | PackageName:
350 | active: true
351 | autoCorrect: true
352 | ParameterListWrapping:
353 | active: true
354 | autoCorrect: true
355 | indentSize: 4
356 | maxLineLength: 120
357 | SpacingAroundAngleBrackets:
358 | active: true
359 | autoCorrect: true
360 | SpacingAroundColon:
361 | active: true
362 | autoCorrect: true
363 | SpacingAroundComma:
364 | active: true
365 | autoCorrect: true
366 | SpacingAroundCurly:
367 | active: true
368 | autoCorrect: true
369 | SpacingAroundDot:
370 | active: true
371 | autoCorrect: true
372 | SpacingAroundDoubleColon:
373 | active: true
374 | autoCorrect: true
375 | SpacingAroundKeyword:
376 | active: true
377 | autoCorrect: true
378 | SpacingAroundOperators:
379 | active: true
380 | autoCorrect: true
381 | SpacingAroundParens:
382 | active: true
383 | autoCorrect: true
384 | SpacingAroundRangeOperator:
385 | active: true
386 | autoCorrect: true
387 | SpacingAroundUnaryOperator:
388 | active: true
389 | autoCorrect: true
390 | SpacingBetweenDeclarationsWithAnnotations:
391 | active: true
392 | autoCorrect: true
393 | SpacingBetweenDeclarationsWithComments:
394 | active: true
395 | autoCorrect: true
396 | StringTemplate:
397 | active: true
398 | autoCorrect: true
399 |
400 | naming:
401 | active: true
402 | BooleanPropertyNaming:
403 | active: true
404 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
405 | allowedPattern: '^(is|has|are)'
406 | ClassNaming:
407 | active: true
408 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
409 | classPattern: '[A-Z][a-zA-Z0-9]*'
410 | ConstructorParameterNaming:
411 | active: true
412 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
413 | parameterPattern: '[a-z][A-Za-z0-9]*'
414 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
415 | excludeClassPattern: '$^'
416 | EnumNaming:
417 | active: true
418 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
419 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
420 | ForbiddenClassName:
421 | active: false
422 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
423 | forbiddenName: []
424 | FunctionMaxLength:
425 | active: false
426 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
427 | maximumFunctionNameLength: 30
428 | FunctionMinLength:
429 | active: false
430 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
431 | minimumFunctionNameLength: 3
432 | FunctionNaming:
433 | active: true
434 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
435 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
436 | ignoreAnnotated: ['Composable']
437 | excludeClassPattern: '$^'
438 | FunctionParameterNaming:
439 | active: true
440 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
441 | parameterPattern: '[a-z][A-Za-z0-9]*'
442 | excludeClassPattern: '$^'
443 | InvalidPackageDeclaration:
444 | active: false
445 | rootPackage: ''
446 | LambdaParameterNaming:
447 | active: true
448 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
449 | parameterPattern: '[a-z][A-Za-z0-9]*|_'
450 | MatchingDeclarationName:
451 | active: true
452 | mustBeFirst: true
453 | MemberNameEqualsClassName:
454 | active: true
455 | ignoreOverridden: true
456 | NoNameShadowing:
457 | active: true
458 | NonBooleanPropertyPrefixedWithIs:
459 | active: true
460 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
461 | ObjectPropertyNaming:
462 | active: true
463 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
464 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
465 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
466 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
467 | PackageNaming:
468 | active: true
469 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
470 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
471 | TopLevelPropertyNaming:
472 | active: true
473 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
474 | constantPattern: '[A-Z][_A-Z0-9]*'
475 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
476 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
477 | VariableMaxLength:
478 | active: false
479 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
480 | maximumVariableNameLength: 64
481 | VariableMinLength:
482 | active: false
483 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
484 | minimumVariableNameLength: 1
485 | VariableNaming:
486 | active: true
487 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
488 | variablePattern: '[a-z][A-Za-z0-9]*'
489 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
490 | excludeClassPattern: '$^'
491 |
492 | performance:
493 | active: true
494 | ArrayPrimitive:
495 | active: true
496 | ForEachOnRange:
497 | active: true
498 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
499 | SpreadOperator:
500 | active: true
501 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
502 | UnnecessaryTemporaryInstantiation:
503 | active: true
504 |
505 | potential-bugs:
506 | active: true
507 | AvoidReferentialEquality:
508 | active: true
509 | forbiddenTypePatterns:
510 | - 'kotlin.String'
511 | CastToNullableType:
512 | active: true
513 | Deprecation:
514 | active: false
515 | DontDowncastCollectionTypes:
516 | active: true
517 | DoubleMutabilityForCollection:
518 | active: true
519 | EqualsAlwaysReturnsTrueOrFalse:
520 | active: true
521 | EqualsWithHashCodeExist:
522 | active: true
523 | ExitOutsideMain:
524 | active: true
525 | ExplicitGarbageCollectionCall:
526 | active: true
527 | HasPlatformType:
528 | active: true
529 | IgnoredReturnValue:
530 | active: true
531 | restrictToConfig: true
532 | returnValueAnnotations:
533 | - '*.CheckResult'
534 | - '*.CheckReturnValue'
535 | ignoreReturnValueAnnotations:
536 | - '*.CanIgnoreReturnValue'
537 | ImplicitDefaultLocale:
538 | active: true
539 | ImplicitUnitReturnType:
540 | active: false
541 | allowExplicitReturnType: true
542 | InvalidRange:
543 | active: true
544 | IteratorHasNextCallsNextMethod:
545 | active: true
546 | IteratorNotThrowingNoSuchElementException:
547 | active: true
548 | LateinitUsage:
549 | active: false
550 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
551 | ignoreOnClassesPattern: ''
552 | MapGetWithNotNullAssertionOperator:
553 | active: true
554 | MissingPackageDeclaration:
555 | active: true
556 | excludes: ['**/*.kts']
557 | NullableToStringCall:
558 | active: true
559 | UnconditionalJumpStatementInLoop:
560 | active: true
561 | UnnecessaryNotNullOperator:
562 | active: true
563 | UnnecessarySafeCall:
564 | active: true
565 | UnreachableCatchBlock:
566 | active: true
567 | UnreachableCode:
568 | active: true
569 | UnsafeCallOnNullableType:
570 | active: true
571 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
572 | UnsafeCast:
573 | active: true
574 | UnusedUnaryOperator:
575 | active: true
576 | UselessPostfixExpression:
577 | active: true
578 | WrongEqualsTypeParameter:
579 | active: true
580 |
581 | style:
582 | active: true
583 | BracesOnWhenStatements:
584 | active: false
585 | ClassOrdering:
586 | active: true
587 | CollapsibleIfStatements:
588 | active: true
589 | DataClassContainsFunctions:
590 | active: false
591 | conversionFunctionPrefix: ['to']
592 | DataClassShouldBeImmutable:
593 | active: true
594 | DestructuringDeclarationWithTooManyEntries:
595 | active: true
596 | maxDestructuringEntries: 3
597 | EqualsNullCall:
598 | active: true
599 | EqualsOnSignatureLine:
600 | active: true
601 | ExplicitCollectionElementAccessMethod:
602 | active: true
603 | ExplicitItLambdaParameter:
604 | active: true
605 | # TODO: https://github.com/detekt/detekt/issues/950#issuecomment-401575701
606 | ExpressionBodySyntax:
607 | active: false
608 | includeLineWrapping: false
609 | ForbiddenComment:
610 | active: true
611 | comments:
612 | - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
613 | value: 'FIXME:'
614 | - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
615 | value: 'STOPSHIP:'
616 | allowedPatterns: ''
617 | ForbiddenImport:
618 | active: false
619 | imports: []
620 | forbiddenPatterns: ''
621 | ForbiddenMethodCall:
622 | active: true
623 | methods:
624 | - 'kotlin.io.print'
625 | - 'kotlin.io.println'
626 | ForbiddenVoid:
627 | active: true
628 | ignoreOverridden: false
629 | ignoreUsageInGenerics: false
630 | FunctionOnlyReturningConstant:
631 | active: true
632 | ignoreOverridableFunction: true
633 | ignoreActualFunction: true
634 | excludedFunctions: ['']
635 | LoopWithTooManyJumpStatements:
636 | active: true
637 | maxJumpCount: 1
638 | MagicNumber:
639 | active: true
640 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
641 | ignoreNumbers:
642 | - '-1'
643 | - '0'
644 | - '0.5'
645 | - '1'
646 | - '2'
647 | ignoreHashCodeFunction: true
648 | ignorePropertyDeclaration: true
649 | ignoreLocalVariableDeclaration: false
650 | ignoreConstantDeclaration: true
651 | ignoreCompanionObjectPropertyDeclaration: true
652 | ignoreAnnotation: false
653 | ignoreNamedArgument: true
654 | ignoreEnums: false
655 | ignoreRanges: false
656 | ignoreExtensionFunctions: true
657 | MandatoryBracesLoops:
658 | active: true
659 | MaxLineLength:
660 | active: true
661 | maxLineLength: 120
662 | excludePackageStatements: true
663 | excludeImportStatements: true
664 | excludeCommentStatements: true
665 | MayBeConst:
666 | active: true
667 | ModifierOrder:
668 | active: true
669 | MultilineLambdaItParameter:
670 | active: true
671 | NestedClassesVisibility:
672 | active: true
673 | NewLineAtEndOfFile:
674 | active: true
675 | NoTabs:
676 | active: true
677 | ObjectLiteralToLambda:
678 | active: true
679 | OptionalAbstractKeyword:
680 | active: true
681 | OptionalUnit:
682 | active: true
683 | PreferToOverPairSyntax:
684 | active: true
685 | ProtectedMemberInFinalClass:
686 | active: true
687 | RedundantExplicitType:
688 | active: true
689 | RedundantHigherOrderMapUsage:
690 | active: true
691 | RedundantVisibilityModifierRule:
692 | active: false # Conflicts with explicit API mode
693 | ReturnCount:
694 | active: true
695 | max: 2
696 | excludedFunctions: ['equals']
697 | excludeLabeled: false
698 | excludeReturnFromLambda: true
699 | excludeGuardClauses: false
700 | SafeCast:
701 | active: true
702 | SerialVersionUIDInSerializableClass:
703 | active: true
704 | SpacingBetweenPackageAndImports:
705 | active: false # See formatting.NoConsecutiveBlankLines
706 | ThrowsCount:
707 | active: true
708 | max: 2
709 | excludeGuardClauses: false
710 | TrailingWhitespace:
711 | active: false # See formatting.NoTrailingWhitespace
712 | UnderscoresInNumericLiterals:
713 | active: true
714 | acceptableLength: 5
715 | UnnecessaryAbstractClass:
716 | active: true
717 | UnnecessaryAnnotationUseSiteTarget:
718 | active: true
719 | UnnecessaryApply:
720 | active: true
721 | UnnecessaryFilter:
722 | active: true
723 | UnnecessaryInheritance:
724 | active: true
725 | UnnecessaryLet:
726 | active: true
727 | UnnecessaryParentheses:
728 | active: true
729 | UntilInsteadOfRangeTo:
730 | active: true
731 | UnusedImports:
732 | active: false # See formatting.NoUnusedImports
733 | UnusedPrivateClass:
734 | active: true
735 | UnusedPrivateMember:
736 | active: true
737 | allowedNames: '(_|ignored|expected|serialVersionUID)'
738 | UseAnyOrNoneInsteadOfFind:
739 | active: true
740 | UseArrayLiteralsInAnnotations:
741 | active: true
742 | UseCheckNotNull:
743 | active: true
744 | UseCheckOrError:
745 | active: true
746 | UseDataClass:
747 | active: false
748 | allowVars: false
749 | UseEmptyCounterpart:
750 | active: true
751 | UseIfEmptyOrIfBlank:
752 | active: true
753 | UseIfInsteadOfWhen:
754 | active: true
755 | UseIsNullOrEmpty:
756 | active: true
757 | UseOrEmpty:
758 | active: true
759 | UseRequire:
760 | active: true
761 | UseRequireNotNull:
762 | active: true
763 | UselessCallOnNotNull:
764 | active: true
765 | UtilityClassWithPublicConstructor:
766 | active: true
767 | VarCouldBeVal:
768 | active: true
769 | WildcardImport:
770 | active: false
771 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
772 | excludeImports:
773 | - 'java.util.*'
774 |
775 | libraries:
776 | ForbiddenPublicDataClass:
777 | active: true
778 | excludes: [ '**' ]
779 | ignorePackages:
780 | - '*.internal'
781 | - '*.internal.*'
782 | LibraryCodeMustSpecifyReturnType:
783 | active: true
784 | excludes: [ '**' ]
785 | LibraryEntitiesShouldNotBePublic:
786 | active: true
787 | excludes: [ '**' ]
788 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | group=com.redmadrobot.konfeature
3 | version=0.1.0
4 |
5 | # IDE (e.g. Android Studio) users:
6 | # Gradle settings configured through the IDE *will override*
7 | # any settings specified in this file.
8 |
9 | # For more details on how to configure your build environment visit
10 | # http://www.gradle.org/docs/current/userguide/build_environment.html
11 |
12 | # Specifies the JVM arguments used for the daemon process.
13 | # The setting is particularly useful for tweaking memory settings.
14 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
15 | org.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8
16 |
17 | # When configured, Gradle will run in incubating parallel mode.
18 | # This option should only be used with decoupled projects. More details, visit
19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
20 | org.gradle.parallel=true
21 |
22 | # Gradle will try to reuse outputs from previous builds for all builds, unless
23 | # explicitly disabled with --no-build-cache.
24 | # https://docs.gradle.org/current/userguide/build_cache.html
25 | org.gradle.caching=true
26 |
27 | # To detect changes on the file-system, and to calculate what needs to be rebuilt, Gradle collects
28 | # information about the file-system in-memory during every build (aka Virtual File-System).
29 | # https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:daemon_watch_fs
30 | org.gradle.vfs.watch=true
31 | #org.gradle.vfs.verbose=true
32 |
33 | # Notify Android Gradle Plugin that we use AndroidX and not use Jetifier
34 | android.useAndroidX=true
35 | android.enableJetifier=false
36 |
37 | # Disable Android Studio version check to be able to use Intellij IDEA.
38 | android.injected.studio.version.check=false
39 |
40 | # Use official kotlin code style
41 | kotlin.code.style=official
42 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | # For an example of how to maintain dependencies in version catalogs,
2 | # see https://github.com/RedMadRobot/gradle-version-catalogs.
3 | [versions]
4 | activity = "1.9.0"
5 | android-gradle-plugin = "8.4.0"
6 | detekt = "1.23.6"
7 | gradle-android-cacheFix = "3.0.1"
8 | gradle-infrastructure = "0.18.1"
9 | kotlin = "2.0.0"
10 | versionsPlugin = "0.51.0"
11 | publish-plugin = "0.28.0"
12 | poko = "0.16.0"
13 | kotest = "5.9.1"
14 |
15 | [libraries]
16 | android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
17 | detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
18 | gradle-android-cacheFixGradlePlugin = { module = "gradle.plugin.org.gradle.android:android-cache-fix-gradle-plugin", version.ref = "gradle-android-cacheFix" }
19 | infrastructure-android = { module = "com.redmadrobot.build:infrastructure-android", version.ref = "gradle-infrastructure" }
20 | infrastructure-publish = { module = "com.redmadrobot.build:infrastructure-publish", version.ref = "gradle-infrastructure" }
21 | publish-gradlePlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish-plugin" }
22 | kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
23 | kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
24 |
25 | [plugins]
26 | infrastructure-detekt = { id = "com.redmadrobot.detekt", version.ref = "gradle-infrastructure" }
27 | versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" }
28 | poko = { id = "dev.drewhamilton.poko", version.ref = "poko" }
29 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/Konfeature/8e01fc7511ecefd3d16563cb0025aca541b4c1e0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/konfeature/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | alias(libs.plugins.poko)
4 | convention.publishing
5 | convention.detekt
6 | }
7 |
8 | description = "Kotlin library for working with feature remote configuration"
9 |
10 | kotlin {
11 | explicitApi()
12 | jvm()
13 |
14 | sourceSets {
15 | commonMain.dependencies {
16 | api(kotlin("stdlib"))
17 | }
18 | commonTest.dependencies {
19 | implementation(kotlin("test"))
20 | implementation(libs.kotest.assertions.core)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfig.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | import com.redmadrobot.konfeature.source.SourceSelectionStrategy
4 | import kotlin.properties.ReadOnlyProperty
5 | import kotlin.reflect.KProperty
6 |
7 | public abstract class FeatureConfig(
8 | override val name: String,
9 | override val description: String
10 | ) : FeatureConfigSpec {
11 | private var konfeature: Konfeature? = null
12 | private val _values = mutableListOf>()
13 |
14 | override val values: List>
15 | get() = _values.toList()
16 |
17 | internal fun bind(konfeature: Konfeature) {
18 | this.konfeature = konfeature
19 | }
20 |
21 | @JvmName("ValueBoolean")
22 | @Deprecated(
23 | message = "Use toggle instead",
24 | replaceWith = ReplaceWith("toggle(key, description, defaultValue, sourceSelectionStrategy)"),
25 | level = DeprecationLevel.ERROR,
26 | )
27 | @Suppress("UNUSED_PARAMETER", "FINAL_UPPER_BOUND")
28 | public fun value(
29 | key: String,
30 | description: String,
31 | defaultValue: T,
32 | sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None
33 | ): ReadOnlyProperty {
34 | error("Use toggle instead of boolean value")
35 | }
36 |
37 | public fun value(
38 | key: String,
39 | description: String,
40 | defaultValue: T,
41 | sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None
42 | ): ReadOnlyProperty {
43 | return createValue(
44 | key = key,
45 | description = description,
46 | defaultValue = defaultValue,
47 | sourceSelectionStrategy = sourceSelectionStrategy
48 | )
49 | }
50 |
51 | public fun toggle(
52 | key: String,
53 | description: String,
54 | defaultValue: Boolean,
55 | sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None,
56 | ): ReadOnlyProperty {
57 | return createValue(
58 | key = key,
59 | description = description,
60 | defaultValue = defaultValue,
61 | sourceSelectionStrategy = sourceSelectionStrategy
62 | )
63 | }
64 |
65 | private fun createValue(
66 | key: String,
67 | description: String,
68 | defaultValue: T,
69 | sourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.None
70 | ): ReadOnlyProperty {
71 | val spec = FeatureValueSpec(
72 | key = key,
73 | description = description,
74 | defaultValue = defaultValue,
75 | sourceSelectionStrategy = sourceSelectionStrategy
76 | )
77 | _values.add(spec)
78 | return Value(spec)
79 | }
80 |
81 | private class Value(
82 | private val spec: FeatureValueSpec,
83 | ) : ReadOnlyProperty {
84 | override fun getValue(thisRef: FeatureConfig?, property: KProperty<*>): T {
85 | return checkBinding(thisRef?.konfeature).getValue(spec).value
86 | }
87 |
88 | private fun checkBinding(konFeature: Konfeature?): Konfeature {
89 | return checkNotNull(konFeature) { "FeatureConfig is not bound to Konfeature" }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureConfigSpec.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | public interface FeatureConfigSpec {
4 | public val name: String
5 | public val description: String
6 | public val values: List>
7 | }
8 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValue.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | import com.redmadrobot.konfeature.source.FeatureValueSource
4 | import dev.drewhamilton.poko.Poko
5 |
6 | @Poko
7 | public class FeatureValue(
8 | public val source: FeatureValueSource,
9 | public val value: T,
10 | )
11 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/FeatureValueSpec.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | import com.redmadrobot.konfeature.source.SourceSelectionStrategy
4 | import dev.drewhamilton.poko.Poko
5 |
6 | @Poko
7 | public class FeatureValueSpec(
8 | public val key: String,
9 | public val description: String,
10 | public val defaultValue: T,
11 | public val sourceSelectionStrategy: SourceSelectionStrategy
12 | )
13 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Konfeature.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | public interface Konfeature {
4 |
5 | public val spec: List
6 |
7 | public fun getValue(spec: FeatureValueSpec): FeatureValue
8 | }
9 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | public interface Logger {
4 |
5 | public fun log(severity: Severity, message: String)
6 |
7 | public enum class Severity {
8 | WARNING, INFO
9 | }
10 | }
11 |
12 | internal fun Logger.logWarn(message: String) {
13 | log(Logger.Severity.WARNING, message)
14 | }
15 |
16 | internal fun Logger.logInfo(message: String) {
17 | log(Logger.Severity.INFO, message)
18 | }
19 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.builder
2 |
3 | import com.redmadrobot.konfeature.*
4 | import com.redmadrobot.konfeature.exception.ConfigNameAlreadyExistException
5 | import com.redmadrobot.konfeature.exception.KeyDuplicationException
6 | import com.redmadrobot.konfeature.exception.NoFeatureConfigException
7 | import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException
8 | import com.redmadrobot.konfeature.source.FeatureSource
9 | import com.redmadrobot.konfeature.source.Interceptor
10 |
11 | public class KonfeatureBuilder {
12 | private val sources = mutableListOf()
13 | private var interceptors = mutableListOf()
14 | private var spec = mutableListOf()
15 | private var logger: Logger? = null
16 |
17 | public fun addInterceptor(interceptor: Interceptor): KonfeatureBuilder {
18 | interceptors.add(interceptor)
19 | return this
20 | }
21 |
22 | public fun addSource(source: FeatureSource): KonfeatureBuilder {
23 | if (sources.any { it.name == source.name }) {
24 | throw SourceNameAlreadyExistException(source.name)
25 | }
26 |
27 | sources.add(source)
28 | return this
29 | }
30 |
31 | public fun register(featureConfig: FeatureConfig): KonfeatureBuilder {
32 | if (spec.any { it.name == featureConfig.name }) {
33 | throw ConfigNameAlreadyExistException(featureConfig.name)
34 | }
35 | spec.add(featureConfig)
36 | return this
37 | }
38 |
39 | public fun setLogger(logger: Logger): KonfeatureBuilder {
40 | this.logger = logger
41 | return this
42 | }
43 |
44 | public fun build(): Konfeature {
45 | if (spec.isEmpty()) throw NoFeatureConfigException()
46 |
47 | spec.forEach(::validateConfigSpec)
48 |
49 | return KonfeatureImpl(
50 | sources = sources,
51 | interceptors = interceptors,
52 | logger = logger,
53 | spec = spec
54 | ).also { toggleEase ->
55 | spec.forEach { values ->
56 | values.bind(toggleEase)
57 | }
58 | }
59 | }
60 |
61 | private fun validateConfigSpec(config: FeatureConfigSpec) {
62 | val counter = mutableMapOf().withDefault { 0 }
63 | var hasDuplicates = false
64 | config.values.forEach { valueSpec ->
65 | val value = counter.getValue(valueSpec.key)
66 | if (value > 0) {
67 | hasDuplicates = true
68 | }
69 | counter[valueSpec.key] = value + 1
70 | }
71 |
72 | if (hasDuplicates) {
73 | val values = counter.asSequence()
74 | .filter { it.value > 1 }
75 | .map { it.key }
76 | .toList()
77 | throw KeyDuplicationException(values, config.name)
78 | } else if (counter.isEmpty()) {
79 | logger?.logWarn("Config '${config.name}' is empty")
80 | }
81 | }
82 | }
83 |
84 | public fun konfeature(build: KonfeatureBuilder.() -> Unit): Konfeature {
85 | return KonfeatureBuilder().apply(build).build()
86 | }
87 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/builder/KonfeatureImpl.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NoWildcardImports", "WildcardImport")
2 |
3 | package com.redmadrobot.konfeature.builder
4 |
5 | import com.redmadrobot.konfeature.*
6 | import com.redmadrobot.konfeature.FeatureConfigSpec
7 | import com.redmadrobot.konfeature.FeatureValueSpec
8 | import com.redmadrobot.konfeature.source.FeatureSource
9 | import com.redmadrobot.konfeature.source.FeatureValueSource
10 | import com.redmadrobot.konfeature.source.Interceptor
11 | import kotlin.reflect.KClass
12 |
13 | internal class KonfeatureImpl(
14 | private val sources: List,
15 | private val interceptors: List,
16 | private val logger: Logger?,
17 | override val spec: List,
18 | ) : Konfeature {
19 |
20 | private val sourcesNames = sources.map { it.name }.toSet()
21 |
22 | @Suppress("LoopWithTooManyJumpStatements")
23 | override fun getValue(spec: FeatureValueSpec): FeatureValue {
24 | val selectedSourcesNames = spec.sourceSelectionStrategy.select(sourcesNames)
25 |
26 | val expectedClass = spec.defaultValue::class
27 | var value: T = spec.defaultValue
28 | var valueSource: FeatureValueSource = FeatureValueSource.Default
29 |
30 | for (source in sources) {
31 | if (source.name !in selectedSourcesNames) continue
32 | val actualSourceValue = source.get(spec.key)
33 | val sourceValue = expectedClass.tryCastOrNull(actualSourceValue)
34 |
35 | if (actualSourceValue != null && sourceValue == null) {
36 | logger?.logUnexpectedValueType(
37 | key = spec.key,
38 | source = FeatureValueSource.Source(source.name),
39 | value = actualSourceValue,
40 | actualClass = actualSourceValue::class.qualifiedName,
41 | expectedClass = expectedClass.qualifiedName,
42 | )
43 | }
44 |
45 | if (sourceValue != null) {
46 | value = sourceValue
47 | valueSource = FeatureValueSource.Source(source.name)
48 | break
49 | }
50 | }
51 |
52 | for (interceptor in interceptors) {
53 | val actualInterceptorValue = interceptor.intercept(valueSource, spec.key, value)
54 | val interceptorValue = expectedClass.tryCastOrNull(actualInterceptorValue)
55 |
56 | if (actualInterceptorValue != null && interceptorValue == null) {
57 | logger?.logUnexpectedValueType(
58 | key = spec.key,
59 | source = FeatureValueSource.Interceptor(interceptor.name),
60 | value = actualInterceptorValue,
61 | actualClass = actualInterceptorValue::class.qualifiedName,
62 | expectedClass = expectedClass.qualifiedName,
63 | )
64 | }
65 |
66 | if (interceptorValue != null) {
67 | value = interceptorValue
68 | valueSource = FeatureValueSource.Interceptor(interceptor.name)
69 | }
70 | }
71 |
72 | logger?.logInfo("Get value '$value' by key '${spec.key}' from '$valueSource'")
73 |
74 | return FeatureValue(valueSource, value)
75 | }
76 |
77 | private fun Logger.logUnexpectedValueType(
78 | key: String,
79 | source: FeatureValueSource,
80 | value: Any,
81 | actualClass: String?,
82 | expectedClass: String?,
83 | ) {
84 | logWarn(
85 | "Unexpected value type for '$key': " +
86 | "expected type is '$expectedClass', but " +
87 | "value from '$source' " +
88 | "is '$value' " +
89 | "with type '$actualClass'",
90 | )
91 | }
92 |
93 | @Suppress("UNCHECKED_CAST")
94 | private fun KClass.tryCastOrNull(value: Any?): T? {
95 | return if (isInstance(value)) value as T else null
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/exception/KonfeatureException.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.exception
2 |
3 | public sealed class KonfeatureException(messageProvider: () -> String) : Exception(messageProvider.invoke())
4 |
5 | public class ConfigNameAlreadyExistException(
6 | name: String
7 | ) : KonfeatureException({ "feature config with name '$name' already registered" })
8 |
9 | public class KeyDuplicationException(
10 | values: List,
11 | config: String
12 | ) : KonfeatureException({
13 | val duplicatedValues = values.joinToString(separator = ", ", transform = { "'$it'" })
14 | "values with keys <$duplicatedValues> are duplicated in config '$config'"
15 | })
16 |
17 | public class NoFeatureConfigException : KonfeatureException({ "No feature config added" })
18 |
19 | public class SourceNameAlreadyExistException(
20 | name: String
21 | ) : KonfeatureException({ "source with name '$name' already registered" })
22 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureSource.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.source
2 |
3 | public interface FeatureSource {
4 |
5 | public val name: String
6 |
7 | public fun get(key: String): Any?
8 | }
9 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/FeatureValueSource.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.source
2 |
3 | import dev.drewhamilton.poko.Poko
4 |
5 | public sealed class FeatureValueSource {
6 |
7 | @Poko
8 | public class Source(public val name: String) : FeatureValueSource()
9 |
10 | @Poko
11 | public class Interceptor(public val name: String) : FeatureValueSource()
12 |
13 | @Suppress("ConvertObjectToDataObject")
14 | public object Default : FeatureValueSource() {
15 | override fun toString(): String = "Default"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/Interceptor.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.source
2 |
3 | public interface Interceptor {
4 |
5 | public val name: String
6 |
7 | public fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any?
8 | }
9 |
--------------------------------------------------------------------------------
/konfeature/src/commonMain/kotlin/com/redmadrobot/konfeature/source/SourceSelectionStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.source
2 |
3 | public fun interface SourceSelectionStrategy {
4 |
5 | public fun select(names: Set): Set
6 |
7 | public companion object {
8 | public val None: SourceSelectionStrategy = SourceSelectionStrategy { emptySet() }
9 | public val Any: SourceSelectionStrategy = SourceSelectionStrategy { it }
10 |
11 | public fun anyOf(vararg sources: String): SourceSelectionStrategy = SourceSelectionStrategy { sources.toSet() }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/KonfeatureTest.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature
2 |
3 | import com.redmadrobot.konfeature.builder.konfeature
4 | import com.redmadrobot.konfeature.helper.TestFeatureConfig
5 | import com.redmadrobot.konfeature.helper.createTestSource
6 | import com.redmadrobot.konfeature.source.FeatureValueSource
7 | import com.redmadrobot.konfeature.source.Interceptor
8 | import com.redmadrobot.konfeature.source.SourceSelectionStrategy
9 | import io.kotest.assertions.assertSoftly
10 | import io.kotest.matchers.shouldBe
11 | import kotlin.test.Test
12 |
13 | class KonfeatureTest {
14 |
15 | @Test
16 | fun `when correct config added - then code should pass`() {
17 | val featureConfig = TestFeatureConfig()
18 |
19 | konfeature {
20 | register(featureConfig)
21 | }
22 | }
23 |
24 | @Test
25 | fun `when correct config added - should be correct spec`() {
26 | // GIVEN
27 | val sourceNames = listOf("Test Source 1", "Test Source 2", "Test Source 3")
28 | val selectedSource = sourceNames[2]
29 |
30 | val featureConfig = TestFeatureConfig(
31 | cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(selectedSource),
32 | )
33 |
34 | // WHEN
35 | val toggleEase = konfeature {
36 | register(featureConfig)
37 | }
38 |
39 | // THEN
40 | toggleEase.spec.size shouldBe 1
41 |
42 | val config = toggleEase.spec.first()
43 |
44 | config.name shouldBe featureConfig.name
45 | config.description shouldBe featureConfig.description
46 | config.values.size shouldBe 3
47 |
48 | assertSoftly(config) {
49 | values[0].apply {
50 | key shouldBe "a"
51 | description shouldBe "feature a desc"
52 | defaultValue shouldBe true
53 | sourceSelectionStrategy.select(sourceNames.toSet()).size shouldBe sourceNames.size
54 | }
55 |
56 | values[1].apply {
57 | key shouldBe "b"
58 | description shouldBe "feature b desc"
59 | defaultValue shouldBe true
60 | sourceSelectionStrategy.select(sourceNames.toSet()).size shouldBe 0
61 | }
62 |
63 | values[2].apply {
64 | key shouldBe "c"
65 | description shouldBe "feature c desc"
66 | defaultValue shouldBe "feature c"
67 | sourceSelectionStrategy.select(sourceNames.toSet()).first() shouldBe selectedSource
68 | }
69 | }
70 | }
71 |
72 | @Test
73 | fun `when source have value - config should return it`() {
74 | // GIVEN
75 | val source = createTestSource(
76 | name = "Test source",
77 | values = mapOf("a" to false),
78 | )
79 | val featureConfig = TestFeatureConfig()
80 |
81 | konfeature {
82 | addSource(source)
83 | register(featureConfig)
84 | }
85 |
86 | // WHEN
87 | val a = featureConfig.a
88 |
89 | // THEN
90 | a shouldBe false
91 | }
92 |
93 | @Test
94 | fun `when source don't have value - config should return default value`() {
95 | // GIVEN
96 | val source = createTestSource(
97 | name = "Test source",
98 | values = mapOf("b" to false),
99 | )
100 | val featureConfig = TestFeatureConfig()
101 |
102 | konfeature {
103 | addSource(source)
104 | register(featureConfig)
105 | }
106 |
107 | // WHEN
108 | val a = featureConfig.a
109 |
110 | // THEN
111 | a shouldBe true
112 | }
113 |
114 | @Test
115 | fun `when source have value with unexpected type - config should return default value`() {
116 | // GIVEN
117 | val source = createTestSource(
118 | name = "Test source",
119 | values = mapOf("a" to 5),
120 | )
121 | val featureConfig = TestFeatureConfig()
122 |
123 | konfeature {
124 | addSource(source)
125 | register(featureConfig)
126 | }
127 |
128 | // WHEN
129 | val a = featureConfig.a
130 |
131 | // THEN
132 | a shouldBe true
133 | }
134 |
135 | @Test
136 | fun `when both sources contain same key - config should return value of first added source`() {
137 | // GIVEN
138 | val source1 = createTestSource(
139 | name = "Test source 1",
140 | values = mapOf("a" to false),
141 | )
142 | val source2 = createTestSource(
143 | name = "Test source 2",
144 | values = mapOf("a" to true),
145 | )
146 |
147 | val featureConfig = TestFeatureConfig()
148 |
149 | // WHEN
150 | konfeature {
151 | addSource(source1)
152 | addSource(source2)
153 | register(featureConfig)
154 | }
155 |
156 | val a = featureConfig.a
157 |
158 | // THEN
159 | a shouldBe false
160 | }
161 |
162 | @Test
163 | fun `when source specified by SourceSelectionStrategy - config should return value from it`() {
164 | // GIVEN
165 | val source1 = createTestSource(
166 | name = "Test source 1",
167 | values = mapOf("c" to "test_source_1_c"),
168 | )
169 | val source2 = createTestSource(
170 | name = "Test source 2",
171 | values = mapOf("c" to "test_source_2_c"),
172 | )
173 |
174 | val featureConfig = TestFeatureConfig(
175 | cSourceSelectionStrategy = SourceSelectionStrategy.anyOf(source2.name),
176 | )
177 |
178 | // WHEN
179 | konfeature {
180 | addSource(source1)
181 | addSource(source2)
182 | register(featureConfig)
183 | }
184 |
185 | val c = featureConfig.c
186 |
187 | // THEN
188 | c shouldBe "test_source_2_c"
189 | }
190 |
191 | @Test
192 | fun `when value changed by interceptor - config should return it`() {
193 | // GIVEN
194 | val source = createTestSource(
195 | name = "Test source",
196 | values = mapOf(
197 | "a" to false,
198 | "b" to true,
199 | "c" to "test_source_1_c",
200 | ),
201 | )
202 |
203 | val interceptedValue = "intercepted_value_c"
204 |
205 | val interceptor = object : Interceptor {
206 | override val name: String = "test interceptor"
207 |
208 | override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? {
209 | return if (key == "c") interceptedValue else null
210 | }
211 | }
212 |
213 | val featureConfig = TestFeatureConfig()
214 |
215 | // WHEN
216 | konfeature {
217 | addSource(source)
218 | addInterceptor(interceptor)
219 | register(featureConfig)
220 | }
221 |
222 | // THEN
223 | assertSoftly(featureConfig) {
224 | a shouldBe false
225 | b shouldBe true
226 | c shouldBe interceptedValue
227 | }
228 | }
229 |
230 | @Test
231 | fun `when value changed by interceptor but has unexpected type - config should return default`() {
232 | // GIVEN
233 | val interceptor = object : Interceptor {
234 | override val name: String = "test interceptor"
235 |
236 | override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? {
237 | return if (key == "c") 100 else null
238 | }
239 | }
240 |
241 | val featureConfig = TestFeatureConfig()
242 |
243 | // WHEN
244 | konfeature {
245 | addInterceptor(interceptor)
246 | register(featureConfig)
247 | }
248 |
249 | // THEN
250 | featureConfig.c shouldBe "feature c"
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/builder/KonfeatureBuilderTest.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.builder
2 |
3 | import com.redmadrobot.konfeature.exception.ConfigNameAlreadyExistException
4 | import com.redmadrobot.konfeature.exception.KeyDuplicationException
5 | import com.redmadrobot.konfeature.exception.NoFeatureConfigException
6 | import com.redmadrobot.konfeature.exception.SourceNameAlreadyExistException
7 | import com.redmadrobot.konfeature.helper.TestFeatureConfig
8 | import com.redmadrobot.konfeature.helper.createEmptyFeatureConfig
9 | import com.redmadrobot.konfeature.helper.createTestSource
10 | import io.kotest.assertions.throwables.shouldThrow
11 | import io.kotest.matchers.shouldBe
12 | import kotlin.test.Test
13 |
14 | class KonfeatureBuilderTest {
15 |
16 | @Test
17 | fun `when no any feature config registered - should throw exception`() {
18 | shouldThrow {
19 | KonfeatureBuilder().build()
20 | }
21 | }
22 |
23 | @Test
24 | fun `when config with duplicated keys added - should throw exception`() {
25 | val featureConfig = TestFeatureConfig(withDuplicates = true)
26 |
27 | val exception = shouldThrow {
28 | KonfeatureBuilder().register(featureConfig).build()
29 | }
30 |
31 | exception.message shouldBe "values with keys <'a'> are duplicated in config '${featureConfig.name}'"
32 | }
33 |
34 | @Test
35 | fun `when source with same name added twice - should throw exception`() {
36 | val featureConfigName = "Test Feature Config"
37 |
38 | val sourceName = "Test Source"
39 |
40 | val exception = shouldThrow {
41 | konfeature {
42 | addSource(createTestSource(sourceName))
43 | addSource(createTestSource(sourceName))
44 | register(createEmptyFeatureConfig(featureConfigName))
45 | }
46 | }
47 |
48 | exception.message shouldBe "source with name '$sourceName' already registered"
49 | }
50 |
51 | @Test
52 | fun `when feature config with same name registered twice - should throw exception`() {
53 | val featureConfigName = "Test Feature Config"
54 |
55 | val exception = shouldThrow {
56 | konfeature {
57 | register(createEmptyFeatureConfig(featureConfigName))
58 | register(createEmptyFeatureConfig(featureConfigName))
59 | }
60 | }
61 |
62 | exception.message shouldBe "feature config with name '$featureConfigName' already registered"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/KonfeatureTestHelper.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.helper
2 |
3 | import com.redmadrobot.konfeature.FeatureConfig
4 | import com.redmadrobot.konfeature.source.FeatureSource
5 |
6 | fun createTestSource(
7 | name: String,
8 | values: Map = emptyMap(),
9 | ): FeatureSource {
10 | return object : FeatureSource {
11 |
12 | override val name: String = name
13 |
14 | override fun get(key: String): Any? = values[key]
15 | }
16 | }
17 |
18 | fun createEmptyFeatureConfig(
19 | name: String,
20 | description: String = "test description for $name",
21 | ): FeatureConfig {
22 | return object : FeatureConfig(name = name, description = description) {}
23 | }
24 |
--------------------------------------------------------------------------------
/konfeature/src/commonTest/kotlin/com/redmadrobot/konfeature/helper/TestFeatureConfig.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.helper
2 |
3 | import com.redmadrobot.konfeature.FeatureConfig
4 | import com.redmadrobot.konfeature.source.SourceSelectionStrategy
5 |
6 | class TestFeatureConfig(
7 | withDuplicates: Boolean = false,
8 | cSourceSelectionStrategy: SourceSelectionStrategy = SourceSelectionStrategy.Any,
9 | ) : FeatureConfig(
10 | name = "TestFeatureConfig",
11 | description = "TestFeatureConfig description",
12 | ) {
13 | val a by toggle(
14 | key = "a",
15 | description = "feature a desc",
16 | defaultValue = true,
17 | sourceSelectionStrategy = SourceSelectionStrategy.Any,
18 | )
19 |
20 | val b by toggle(
21 | key = if (withDuplicates) "a" else "b",
22 | description = "feature b desc",
23 | defaultValue = true,
24 | sourceSelectionStrategy = SourceSelectionStrategy.None,
25 | )
26 |
27 | val c: String by value(
28 | key = if (withDuplicates) "a" else "c",
29 | description = "feature c desc",
30 | defaultValue = "feature c",
31 | sourceSelectionStrategy = cSourceSelectionStrategy,
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Prepares the library for release. Creates a release branch from the 'main'.
4 | #
5 | # Usage: ./release.sh [version]
6 | # Example: ./release.sh 1.0.0
7 | #
8 | # Original release script: https://github.com/RedMadRobot/android-library-template/blob/main/release.sh
9 |
10 | set -euo pipefail
11 |
12 | # The script could be run from any directory.
13 | cd "$(dirname "$0")"
14 |
15 | # Configure the script
16 | properties="gradle.properties"
17 | changelog="CHANGELOG.md"
18 | readme="README.md"
19 | files_to_update_version=("$properties" "$readme")
20 | github_repository_url="https://github.com/RedMadRobot/Konfeature"
21 |
22 | #region Utils
23 | function error() {
24 | echo "❌ $1"
25 | return 1
26 | }
27 |
28 | function property {
29 | grep "^${1}=" "$properties" | cut -d'=' -f2
30 | }
31 |
32 | function replace() {
33 | # Escape linebreaks
34 | local replacement=${2//$'\n'/\\\n}
35 | # Portable in-place edit.
36 | # See: https://stackoverflow.com/a/4247319
37 | sed -i".bak" -E "s~$1~$replacement~g" "$3"
38 | rm "$3.bak"
39 | }
40 |
41 | function diff_link() {
42 | echo -n "$github_repository_url/compare/${1}...${2}"
43 | }
44 | #endregion
45 |
46 | # Validate input parameters
47 | version=${1:-Please, specify the version to be released as a script parameter}
48 | [[ $version != v* ]] || error "The version should not start from 'v'"
49 | version_tag="v$version"
50 |
51 | # 0. Fetch remote changes
52 | echo "️⏳ Creating release branch..."
53 | release_branch="release/$version"
54 | git checkout --quiet -b "$release_branch"
55 | git pull --quiet --rebase origin main
56 | echo "✅ Branch '$release_branch' created"
57 | echo
58 |
59 | # 1. Calculate version values for later
60 | last_version=$(property "version")
61 | if [[ "$last_version" == "$version" ]]; then
62 | echo "🤔 Version $version is already set."
63 | exit 0
64 | fi
65 | echo "🚀 Update $last_version → $version"
66 | echo
67 |
68 | # 2. Update version everywhere
69 | for file in "${files_to_update_version[@]}" ; do
70 | replace "$last_version" "$version" "$file"
71 | echo "✅ Updated version in $file"
72 | done
73 |
74 | # 3. Update header in CHANGELOG.md
75 | date=$(date -u +%Y-%m-%d)
76 | header_replacement=\
77 | "## [Unreleased]
78 |
79 | ### Changes
80 |
81 | - *No changes*
82 |
83 | ## [$version] ($date)"
84 | replace "^## \[Unreleased\].*" "$header_replacement" "$changelog"
85 | echo "✅ Updated CHANGELOG.md header"
86 |
87 | # 4. Add link to version diff
88 | unreleased_diff_link="[unreleased]: $(diff_link "$version_tag" "main")"
89 | version_diff_link="[$version]: $(diff_link "v$last_version" "$version_tag")"
90 | replace "^\[unreleased\]:.*" "$unreleased_diff_link\n$version_diff_link" "$changelog"
91 | echo "✅ Added a diff link to CHANGELOG.md"
92 |
93 | # 5. Ask if the changes should be pushed to remote branch
94 | echo
95 | echo "Do you want to commit the changes and push the release branch and tag?"
96 | echo "The release tag push triggers a release workflow on CI."
97 | read -p " Enter 'yes' to continue: " -r input
98 | if [[ "$input" != "yes" ]]; then
99 | echo "👌 SKIPPED."
100 | exit 0
101 | fi
102 |
103 | # 6. Push changes and trigger release on CI
104 | echo
105 | echo "⏳ Pushing the changes to the remote repository..."
106 | git add "$readme" "$changelog" "$properties"
107 | git commit --quiet --message "version: $version"
108 | git tag "$version_tag"
109 | git push --quiet origin HEAD "$version_tag"
110 | echo "🎉 DONE."
111 | echo "Create a Pull Request: $(diff_link "main" "$release_branch")"
112 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | ":semanticCommitScopeDisabled"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | application
4 | convention.detekt
5 | }
6 | dependencies {
7 | implementation(projects.konfeature)
8 | }
9 |
10 | application {
11 | mainClass = "com.redmadrobot.konfeature.sample.AppKt"
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/App.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.sample
2 |
3 | import com.redmadrobot.konfeature.Logger
4 | import com.redmadrobot.konfeature.builder.konfeature
5 |
6 | fun main() {
7 | val featureConfig = SampleFeatureConfig()
8 |
9 | val debugPanelInterceptor = FeatureToggleDebugPanelInterceptor()
10 |
11 | val logger = object : Logger {
12 | override fun log(severity: Logger.Severity, message: String) {
13 | println("${severity.name}: $message")
14 | }
15 | }
16 |
17 | val konfeature = konfeature {
18 | addSource(RemoteFeatureSource())
19 | addSource(FirebaseFeatureSource())
20 | register(featureConfig)
21 | addInterceptor(debugPanelInterceptor)
22 | setLogger(logger)
23 | }
24 |
25 | konfeature.spec.forEach {
26 | println("Spec: --name: '${it.name}', description: '${it.description}'")
27 | it.values.forEach(::println)
28 | }
29 |
30 | println()
31 | val spec = konfeature.spec.first().values.first()
32 | println("getFeatureValue('${spec.key}') -> ${konfeature.getValue(spec)}")
33 |
34 | println()
35 | println("feature1: " + featureConfig.isFeature1Enabled)
36 | println("feature2: " + featureConfig.isFeature2Enabled)
37 | println("feature3: " + featureConfig.isFeature3Enabled)
38 | println("velocity: " + featureConfig.velocity)
39 | println("puhFeature: " + featureConfig.puhFeature)
40 |
41 | debugPanelInterceptor.setFeatureValue("feature2", false)
42 | println()
43 | println("debugPanelInterceptor.setFeatureValue(\"feature2\", false)")
44 | println("feature1: " + featureConfig.isFeature1Enabled)
45 | println("feature2: " + featureConfig.isFeature2Enabled)
46 | println("feature3: " + featureConfig.isFeature3Enabled)
47 | }
48 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/FeatureToggleDebugPanelInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.sample
2 |
3 | import com.redmadrobot.konfeature.source.FeatureValueSource
4 | import com.redmadrobot.konfeature.source.Interceptor
5 |
6 | class FeatureToggleDebugPanelInterceptor : Interceptor {
7 |
8 | private val values = mutableMapOf()
9 |
10 | override val name: String = "DebugPanelInterceptor"
11 |
12 | override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? {
13 | return values[key]
14 | }
15 |
16 | fun setFeatureValue(key: String, value: Any) {
17 | values[key] = value
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleFeatureConfig.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.sample
2 |
3 | import com.redmadrobot.konfeature.FeatureConfig
4 | import com.redmadrobot.konfeature.source.SourceSelectionStrategy
5 |
6 | class SampleFeatureConfig : FeatureConfig(
7 | name = "Sample",
8 | description = "simple sample set"
9 | ) {
10 |
11 | val isFeature1Enabled: Boolean by toggle(
12 | key = "feature1",
13 | description = "feature1 desc",
14 | defaultValue = false,
15 | )
16 |
17 | val isFeature2Enabled: Boolean by toggle(
18 | key = "feature2",
19 | description = "feature2 desc",
20 | defaultValue = true,
21 | sourceSelectionStrategy = SourceSelectionStrategy.Any
22 | )
23 |
24 | val isFeature3Enabled: Boolean by toggle(
25 | key = "feature3",
26 | description = "feature3 desc",
27 | defaultValue = false,
28 | sourceSelectionStrategy = SourceSelectionStrategy.Any
29 | )
30 |
31 | val velocity: Long by value(
32 | key = "velocity_value",
33 | description = "velocity value",
34 | defaultValue = 90,
35 | sourceSelectionStrategy = SourceSelectionStrategy.Any
36 | )
37 |
38 | val isGroupFeatureEnable: Boolean
39 | get() = isFeature1Enabled && isFeature3Enabled
40 |
41 | val feature4: String by value(
42 | key = "feature4",
43 | description = "feature4 desc",
44 | defaultValue = "true",
45 | sourceSelectionStrategy = SourceSelectionStrategy.Any
46 | )
47 |
48 | enum class PUH { A, B, C }
49 |
50 | private val _puhFeature by value(
51 | key = "puhFeature",
52 | description = "puhFeature desc",
53 | defaultValue = PUH.B.name,
54 | sourceSelectionStrategy = SourceSelectionStrategy.Any
55 | )
56 |
57 | val puhFeature: PUH
58 | get() = _puhFeature.let { PUH.valueOf(it) }
59 | }
60 |
--------------------------------------------------------------------------------
/sample/src/main/kotlin/com/redmadrobot/konfeature/sample/SampleSources.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.konfeature.sample
2 |
3 | import com.redmadrobot.konfeature.source.FeatureSource
4 |
5 | class RemoteFeatureSource : FeatureSource {
6 |
7 | private val store = mutableMapOf().apply {
8 | put("feature1", false)
9 | put("feature3", true)
10 | put("velocity_value", true)
11 | }
12 |
13 | override val name: String = "RemoteFeatureToggleSource"
14 |
15 | override fun get(key: String): Any? {
16 | return store[key]
17 | }
18 | }
19 |
20 | class FirebaseFeatureSource : FeatureSource {
21 |
22 | private val store = mutableMapOf().apply {
23 | put("feature2", true)
24 | put("puhFeature", "C")
25 | }
26 |
27 | override val name: String = "FirebaseFeatureToggleSource"
28 |
29 | override fun get(key: String): Any? {
30 | return store[key]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2 |
3 | pluginManagement {
4 | repositories {
5 | google {
6 | content {
7 | includeGroupAndSubgroups("com.android")
8 | includeGroupAndSubgroups("com.google")
9 | includeGroupAndSubgroups("androidx")
10 | }
11 | }
12 | gradlePluginPortal()
13 | }
14 | }
15 |
16 | @Suppress("UnstableApiUsage")
17 | dependencyResolutionManagement {
18 | repositories {
19 | mavenCentral()
20 | }
21 | }
22 |
23 | rootProject.name = "konfeature-root"
24 |
25 | include(
26 | ":sample",
27 | ":konfeature",
28 | )
29 |
--------------------------------------------------------------------------------