unified_settings 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc523c80caf34cfc9c862df454f01a434518c149e94ef342d354c7f5af4e40fe
4
+ data.tar.gz: 42e5d82f9d6b6cb4ee87b49c0ef97c987176bbb688a4f8b2492f6f4f353ff509
5
+ SHA512:
6
+ metadata.gz: d7ead0fae11ea90725007c1b0c8d80c309ad12fe2e25b86181355502efca95631992ffbbee21c8416228b65937238bae242c76ddb170771f5169ca229d3567ec
7
+ data.tar.gz: 96a5d214ba297bee6f0cbc0e8309a34ab30a85ad4d87c91d8941ab06b17954341d6b4bbb0416850f327f642922b32bc3b352908520d778fc9296a571dc78a7d7
data/.rubocop.yml ADDED
@@ -0,0 +1,53 @@
1
+ require:
2
+ - 'rubocop-performance'
3
+ - 'rubocop-rails'
4
+ - 'rubocop-minitest'
5
+ - 'rubocop-rake'
6
+
7
+ AllCops:
8
+ NewCops: enable
9
+ # Don't run rubocop on these files/directories
10
+ Exclude:
11
+ - '**/templates/**/*'
12
+ - '**/vendor/**/*'
13
+ - 'lib/templates/**/*'
14
+ - 'db/**/*'
15
+ - 'config/**/*'
16
+ - 'vendor/**/*'
17
+ - 'bin/**/*'
18
+
19
+ Layout/LineLength:
20
+ Max: 80
21
+ Exclude:
22
+ - 'unified_settings.gemspec'
23
+
24
+ Metrics/AbcSize:
25
+ Max: 30
26
+ Exclude:
27
+ - 'test/**/*'
28
+
29
+ Metrics/BlockLength:
30
+ Max: 40
31
+ Exclude:
32
+ - 'unified_settings.gemspec'
33
+ - 'test/**/*'
34
+
35
+ Metrics/ClassLength:
36
+ Max: 150
37
+ Exclude:
38
+ - 'test/**/*'
39
+
40
+ Metrics/MethodLength:
41
+ Max: 35
42
+ Exclude:
43
+ - 'test/**/*'
44
+
45
+ Metrics/ModuleLength:
46
+ Max: 100
47
+
48
+ Minitest/MultipleAssertions:
49
+ Max: 10
50
+
51
+ Rails/RefuteMethods:
52
+ Exclude:
53
+ - 'test/**/*'
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ unified_settings
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.2.2
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in unified_settings.gemspec
6
+ gemspec
7
+
8
+ gem 'bundler', '~> 2.3'
9
+ gem 'bundler-audit', '>= 0'
10
+ gem 'config', '>= 3.0'
11
+ gem 'minitest', '~> 5.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rubocop', '~> 1.21'
14
+ gem 'rubocop-minitest', '>= 0'
15
+ gem 'rubocop-performance', '>= 0'
16
+ gem 'rubocop-rails', '>= 0'
17
+ gem 'rubocop-rake', '>= 0'
18
+ gem 'ruby_audit', '>= 0'
data/Gemfile.lock ADDED
@@ -0,0 +1,129 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ unified_settings (0.1.0)
5
+ activerecord (> 4.2.0)
6
+ activesupport (> 4.2.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (7.0.6)
12
+ activesupport (= 7.0.6)
13
+ activerecord (7.0.6)
14
+ activemodel (= 7.0.6)
15
+ activesupport (= 7.0.6)
16
+ activesupport (7.0.6)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ ast (2.4.2)
22
+ bundler-audit (0.9.1)
23
+ bundler (>= 1.2.0, < 3)
24
+ thor (~> 1.0)
25
+ concurrent-ruby (1.2.2)
26
+ config (4.2.1)
27
+ deep_merge (~> 1.2, >= 1.2.1)
28
+ dry-validation (~> 1.0, >= 1.0.0)
29
+ deep_merge (1.2.2)
30
+ dry-configurable (1.0.1)
31
+ dry-core (~> 1.0, < 2)
32
+ zeitwerk (~> 2.6)
33
+ dry-core (1.0.0)
34
+ concurrent-ruby (~> 1.0)
35
+ zeitwerk (~> 2.6)
36
+ dry-inflector (1.0.0)
37
+ dry-initializer (3.1.1)
38
+ dry-logic (1.5.0)
39
+ concurrent-ruby (~> 1.0)
40
+ dry-core (~> 1.0, < 2)
41
+ zeitwerk (~> 2.6)
42
+ dry-schema (1.13.2)
43
+ concurrent-ruby (~> 1.0)
44
+ dry-configurable (~> 1.0, >= 1.0.1)
45
+ dry-core (~> 1.0, < 2)
46
+ dry-initializer (~> 3.0)
47
+ dry-logic (>= 1.4, < 2)
48
+ dry-types (>= 1.7, < 2)
49
+ zeitwerk (~> 2.6)
50
+ dry-types (1.7.1)
51
+ concurrent-ruby (~> 1.0)
52
+ dry-core (~> 1.0)
53
+ dry-inflector (~> 1.0)
54
+ dry-logic (~> 1.4)
55
+ zeitwerk (~> 2.6)
56
+ dry-validation (1.10.0)
57
+ concurrent-ruby (~> 1.0)
58
+ dry-core (~> 1.0, < 2)
59
+ dry-initializer (~> 3.0)
60
+ dry-schema (>= 1.12, < 2)
61
+ zeitwerk (~> 2.6)
62
+ i18n (1.14.1)
63
+ concurrent-ruby (~> 1.0)
64
+ json (2.6.3)
65
+ language_server-protocol (3.17.0.3)
66
+ minitest (5.18.1)
67
+ parallel (1.23.0)
68
+ parser (3.2.2.3)
69
+ ast (~> 2.4.1)
70
+ racc
71
+ racc (1.7.1)
72
+ rack (2.2.7)
73
+ rainbow (3.1.1)
74
+ rake (13.0.6)
75
+ regexp_parser (2.8.1)
76
+ rexml (3.2.5)
77
+ rubocop (1.54.2)
78
+ json (~> 2.3)
79
+ language_server-protocol (>= 3.17.0)
80
+ parallel (~> 1.10)
81
+ parser (>= 3.2.2.3)
82
+ rainbow (>= 2.2.2, < 4.0)
83
+ regexp_parser (>= 1.8, < 3.0)
84
+ rexml (>= 3.2.5, < 4.0)
85
+ rubocop-ast (>= 1.28.0, < 2.0)
86
+ ruby-progressbar (~> 1.7)
87
+ unicode-display_width (>= 2.4.0, < 3.0)
88
+ rubocop-ast (1.29.0)
89
+ parser (>= 3.2.1.0)
90
+ rubocop-minitest (0.31.0)
91
+ rubocop (>= 1.39, < 2.0)
92
+ rubocop-performance (1.18.0)
93
+ rubocop (>= 1.7.0, < 2.0)
94
+ rubocop-ast (>= 0.4.0)
95
+ rubocop-rails (2.20.2)
96
+ activesupport (>= 4.2.0)
97
+ rack (>= 1.1)
98
+ rubocop (>= 1.33.0, < 2.0)
99
+ rubocop-rake (0.6.0)
100
+ rubocop (~> 1.0)
101
+ ruby-progressbar (1.13.0)
102
+ ruby_audit (2.2.0)
103
+ bundler-audit (~> 0.9.0)
104
+ thor (1.2.2)
105
+ tzinfo (2.0.6)
106
+ concurrent-ruby (~> 1.0)
107
+ unicode-display_width (2.4.2)
108
+ zeitwerk (2.6.8)
109
+
110
+ PLATFORMS
111
+ x86_64-darwin-22
112
+ x86_64-linux
113
+
114
+ DEPENDENCIES
115
+ bundler (~> 2.3)
116
+ bundler-audit
117
+ config (>= 3.0)
118
+ minitest (~> 5.0)
119
+ rake (~> 13.0)
120
+ rubocop (~> 1.21)
121
+ rubocop-minitest
122
+ rubocop-performance
123
+ rubocop-rails
124
+ rubocop-rake
125
+ ruby_audit
126
+ unified_settings!
127
+
128
+ BUNDLED WITH
129
+ 2.4.10
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 TODO: Write your name
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # UnifiedSettings
2
+
3
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/prschmid/unified_settings/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/prschmid/unified_settings/tree/main)
4
+ [![Gem Version](https://badge.fury.io/rb/unified_settings.svg)](https://badge.fury.io/rb/unified_settings)
5
+
6
+ A simple and unified way to get any setting in your code regardless of where it is defined.
7
+
8
+ ## Installation
9
+
10
+ Install the gem and add to the application's Gemfile by executing:
11
+
12
+ $ bundle add unified_settings
13
+
14
+ If bundler is not being used to manage dependencies, install the gem by executing:
15
+
16
+ $ gem install unified_settings
17
+
18
+ ## Usage
19
+
20
+ ### Basic Usage
21
+
22
+ To get the value of `some_setting`, simply do:
23
+
24
+ UnifiedSettings.get('some_setting')
25
+
26
+ Or to check if the key has been defined:
27
+
28
+ UnifiedSettings.defined?('some_setting')
29
+
30
+ This will search the following locations in the following order:
31
+ 1. ENV
32
+ 2. Rails Credentials (if using Rails)
33
+ 4. constants
34
+
35
+ When using `UnifiedSettings.get('some_setting')`, the *first* setting that matches the provided key will be returned. As such, even if the same key is defined in ENV, Credentials and as a constant, it will return the value defined in ENV as that is what will take precedence.
36
+
37
+ If one wants to change the search order, or limit what is searched, one should provided the handlers to search explicity. For example:
38
+
39
+ UnifiedSettings.get(
40
+ 'some_setting',
41
+ handlers: [
42
+ UnifiedSettings::Handlers::Credentials,
43
+ UnifiedSettings::Handlers::Env
44
+ ]
45
+ )
46
+
47
+ This will first search the Credentials, then ENV, and completely ignore any values that might be defined in Settings or as a constant. If there are any other places that need to be searched, a new custom handler can be created and then provided. To do this, just create a class that inherits from `UnifiedSettings::Handlers::Base` and add it to the list of handlers. For details on how to configure `UnifiedSettings` to use differnet handlers by default, see the Configuration section below.
48
+
49
+ Note, by default the search is also done in a "case insensitive" manner. This means, for each setting source, it will first try to match the key as provied (e.g. `Some_Setting`). If nothing is found it will then attempt an upper case version of the key (`SOME_SETTING`) and a lower case version (`some_setting`). If this is not desired, one can do
50
+
51
+ UnifiedSettings.get('some_setting', case_sensitive: true)
52
+
53
+ ### Predefined Handlers
54
+
55
+ Build in are 4 pre-defined handlers that look for setting keys in predfined locattions
56
+
57
+ * `UnifiedSettings::Handlers::ConfigGem`: Look for settings via the interface provied by [Config](https://github.com/rubyconfig/config)
58
+ * `UnifiedSettings::Handlers::Constants`: Look for a setting defined by a constant
59
+ * `UnifiedSettings::Handlers::Credentials`: Look for a setting in a Rails Credentials file
60
+ * `UnifiedSettings::Handlers::Env`: Look for a setting defined in `ENV`
61
+
62
+ ### Coercing strings to objects
63
+
64
+ In many instances it is only possible to define strings as the value of a setting. For example, when setting an `ENV` var `SUPER_IMPORTANT_IDS` with the value of `1,2,3,4,5`, what the user really wants is a list of numbers, and not a comma separated string. As such, `UnifiedSettings` will automatically try to coerce things that look like arrays into arrays. Furthermore, it will convert things that look like floats to floats, ints to ints, booleans to booleans, etc. This way one does not have to worry about converting the values of settings to be easily used within the application. For example, is `some_setting` had the value of `' string, tRue, false,1, 2.2, NiL '`, the following be returned `['string', true, false, 1, 2.2, nil]`.
65
+
66
+ There are times when coercion is not desired (e.g. for things like long passcodes that may look like arrays since they may contain commas/numbers, etc.). For situations like this, simply disable the coercion.
67
+
68
+ UnifiedSettings.get(`some_setting`, coerce: false)
69
+ ### Handling Missing Keys
70
+
71
+ #### Setting a Default Value
72
+
73
+ In many cases there might be a default value that should be provided if a key is missing. This can be supplied as follows:
74
+
75
+ UnifiedSettings.get('some_setting', default: 'some_value')
76
+
77
+ #### Logging/Raising Error
78
+
79
+ By default, when there is a missing key, an message will be logged with the severity of `error`. Depending on the situation one may want a different behavior. As such, different error handlers can be passed to meet those needs. The following handlers are predefined: `:log_debug`, `:log_info`, `:log_warn`, `:log_error`, `:log_fatal`, `:raise`. For example:
80
+
81
+ UnifiedSettings.get('some_setting', on_missing_key: :raise)
82
+
83
+ If these do not suffice, one can pass an anonymous function that takes `key` as a parameter. For example:
84
+
85
+ UnifiedSettings.get(
86
+ 'some_setting',
87
+ on_missing_key: ->(key) { puts "Something is wrong with #{key}" }
88
+ )
89
+
90
+ Furthermore, one can pass a list of hanlders, so that multiple things can happen. For example
91
+
92
+ UnifiedSettings.get(
93
+ 'some_setting',
94
+ on_missing_key: [
95
+ :log_fatal,
96
+ ->(key) { puts "Something is wrong with #{key}" },
97
+ :raise
98
+ ]
99
+ )
100
+
101
+ This will run the handlers in the order that they were defined.
102
+
103
+ ### Nested Settings
104
+
105
+ In many cases it is advantageous to have settings nested when defining them in, for example, the Rails Credentials file. For example, if we had the following defined in the Credentials file:
106
+
107
+ aws:
108
+ client_id: SOME_ID
109
+ client_secret: SOME_SECRET
110
+
111
+ Then the corresponding setting keys would be:
112
+
113
+ aws.client_id
114
+ aws.client_secret
115
+
116
+ As you can see, for nested settings, the convention to be used is to separate the elements using a '.'
117
+
118
+ The separator for ENV variables follow a slightly different as there can be issues when using a `.` in ENV variable names. As such the convention used in `UnifiedSettings` is modeled after the [Config gem](https://github.com/rubyconfig/config). When defining a value via an environment variable, the separator is a double underscore (`__`). Continuing with the above example, the ENV keys would be
119
+
120
+ AWS__CLIENT_ID
121
+ AWS__CLIENT_SECRET
122
+
123
+ This means, if you do
124
+
125
+ UnifiedSettings.get('aws.client_id')
126
+
127
+ it will look for an ENV var of the form `AWS__CLIENT_ID`.
128
+
129
+ ## Configuration
130
+
131
+ Most of the settings that can be set at the individual call level can also be globally configured.
132
+
133
+ IMPORTANT FOR RAILS: If one is planning on using `UnifiedSettings` during the initialization for other Rails gems, then this configuration MUST be done before we run `application.initialize!`.
134
+
135
+ ### Configuring the Handlers
136
+
137
+ By default, the search order for a settings is:
138
+ 1. ENV
139
+ 2. Rails Credentials (if using Rails)
140
+ 4. constants
141
+
142
+ For example, if one is also using the [Config gem](https://github.com/rubyconfig/config), and would also like to search for settings there, one can add that handler.
143
+
144
+ UnifiedSettings.configure do |config|
145
+ config.handlers = [
146
+ UnifiedSettings::Handlers::Env,
147
+ UnifiedSettings::Handlers::Credentials,
148
+ UnifiedSettings::Handlers::ConfigGem,
149
+ UnifiedSettings::Handlers::Constants
150
+ ]
151
+ end
152
+
153
+ If one needs to supply some extra parameters when initializing the handler, for example if a non-default constant is used for the `Config` gem, one needs only to pass a hash as follows:
154
+
155
+ UnifiedSettings.configure do |config|
156
+ config.handlers = [
157
+ UnifiedSettings::Handlers::Env,
158
+ UnifiedSettings::Handlers::Credentials,
159
+ {
160
+ handler: UnifiedSettings::Handlers::ConfigGem,
161
+ params: {
162
+ const_name: 'ConfigSettings'
163
+ }
164
+ },
165
+ UnifiedSettings::Handlers::Constants
166
+ ]
167
+ end
168
+
169
+ ### All Other Options
170
+
171
+ Instead of going through all the various options, here is a fully worked out example with inline comments.
172
+
173
+ ```
174
+ UnifiedSettings.configure do |config|
175
+ config.handlers = [
176
+ UnifiedSettings::Handlers::Env,
177
+ UnifiedSettings::Handlers::Credentials,
178
+ {
179
+ handler: UnifiedSettings::Handlers::ConfigGem,
180
+ params: {
181
+ const_name: 'Settings'
182
+ }
183
+ },
184
+ UnifiedSettings::Handlers::Constants
185
+ ]
186
+
187
+ # Whether or not keys should be case sensitive. For example, let's assume the
188
+ # key foo.bar.baz is defined in one of the locations the handlers are
189
+ # configured to search. If we use case_sensitive = false then any of the
190
+ # following examples will match
191
+ # UnifiedSettings.get('FOO.BAR.BAZ')
192
+ # UnifiedSettings.get('foo.bar.bar')
193
+ # UnifiedSettings.get('Foo.BaR.baZ')
194
+ # If we set set case_sensitive = true, then only if the case exactly matches
195
+ # the key is a result returned.
196
+ # Default is: config.case_sensitive = false
197
+ config.case_sensitive = false
198
+
199
+ # This can be any of the following pre-defined handlers:
200
+ # :log_debug, :log_info, :log_warn, :log_error, :log_fatal, :raise
201
+ # or you can pass an anonymous function that takes `key` as a parameter. E.g.
202
+ # config.on_missing_key = ->(key) { puts "Something is wrong with #{key}" }
203
+ # If you need multiple things to happen, simply use an Array here. E.g.
204
+ # config.on_missing_key = [
205
+ # :log_fatal,
206
+ # ->(key) { puts "Something is wrong with #{key}" },
207
+ # :raise
208
+ # ]
209
+ # This will run the handlers in the order that they were defined.
210
+ #
211
+ # Default is: config.on_missing_key = :log_error
212
+ config.on_missing_key = :log_error
213
+
214
+ # The value that should be returned if no key was found. This can be a set
215
+ # to a particular value (e.g. `nil`, or `{}`), or can be an anonymous function
216
+ # that takes `key` as a parameter. E.g.
217
+ # config.default_value = ->(key) { key.starts_with?('a') ? 'AA' : 'ZZ'}
218
+ #
219
+ # Default is: config.default_value = nil
220
+ config.default_value = nil
221
+
222
+ # The types that should be coerced from strings to their Ruby type.
223
+ # (e.g. "true" to the boolean true or "1.2" to the float 1.2).
224
+ # Default is: config.coercions = %i[nil boolean integer float]
225
+ config.coercions = %i[nil boolean integer float]
226
+
227
+ # Whether or not to coerce strings that look like arrays to arrays.
228
+ # E.g. "a, B, true,1 " would be coerced become to ["a", "B", true, 1]
229
+ # Defualt is:
230
+ # config.coerce_arrays = true
231
+ # config.coerce_array_separator = ','
232
+ config.coerce_arrays = true
233
+ config.coerce_array_separator = ','
234
+ end
235
+ ```
236
+
237
+ ## Development
238
+
239
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
240
+
241
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
242
+
243
+ ## Contributing
244
+
245
+ Bug reports and pull requests are welcome on GitHub at https://github.com/prschmid/unified_settings.
246
+
247
+ ## License
248
+
249
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ require 'rubocop/rake_task'
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedSettings
4
+ #
5
+ # This will take string values and coerce them to the approrpiate ruby
6
+ # objects (e.g. "true" to the boolean true or "1.2" to the float 1.2)
7
+ #
8
+ class Coercer
9
+ def initialize(
10
+ coercions: %i[nil boolean integer float],
11
+ coerce_arrays: true,
12
+ array_separator: ','
13
+ )
14
+ @coercions = coercions
15
+ @coerce_arrays = coerce_arrays
16
+ @array_separator = array_separator
17
+ end
18
+
19
+ def coerce(value)
20
+ return value unless value
21
+
22
+ # If it's already been cast to something other than a string, just
23
+ # return what it is.
24
+ return value unless value.is_a?(String)
25
+
26
+ stripped_value = value.strip
27
+
28
+ coerced_value, is_array = coerce_to_array(stripped_value)
29
+ if is_array
30
+ coerced_value.map do |array_value|
31
+ array_value = array_value.strip
32
+ coerced_value, did_coerce = coerce_value(array_value)
33
+ did_coerce ? coerced_value : array_value
34
+ end
35
+ else
36
+ coerced_value, did_coerce = coerce_value(stripped_value)
37
+ did_coerce ? coerced_value : stripped_value
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def coerce_value(value)
44
+ return [value, false] unless value
45
+
46
+ coerced_value, did_coerce = coerce_to_nil(value)
47
+ return [coerced_value, did_coerce] if did_coerce
48
+
49
+ coerced_value, did_coerce = coerce_to_boolean(value)
50
+ return [coerced_value, did_coerce] if did_coerce
51
+
52
+ coerced_value, did_coerce = coerce_to_integer(value)
53
+ return [coerced_value, did_coerce] if did_coerce
54
+
55
+ coerced_value, did_coerce = coerce_to_float(value)
56
+ return [coerced_value, did_coerce] if did_coerce
57
+
58
+ [value, false]
59
+ end
60
+
61
+ def coerce_to_array(value)
62
+ return [value, false] unless @coerce_arrays
63
+ return [value, false] unless value.include?(@array_separator)
64
+
65
+ [value.split(@array_separator), true]
66
+ end
67
+
68
+ def coerce_to_nil(value)
69
+ return [value, false] unless @coercions.include?(:nil)
70
+ return [nil, true] if value.casecmp('nil').zero?
71
+
72
+ [value, false]
73
+ end
74
+
75
+ def coerce_to_boolean(value)
76
+ return [value, false] unless @coercions.include?(:boolean)
77
+ return [true, true] if value.casecmp('true').zero?
78
+ return [false, true] if value.casecmp('false').zero?
79
+
80
+ [value, false]
81
+ end
82
+
83
+ def coerce_to_integer(value)
84
+ return [value, false] unless @coercions.include?(:integer)
85
+
86
+ begin
87
+ return [Integer(value), true]
88
+ rescue ArgumentError # rubocop:disable Lint/SuppressedException
89
+ end
90
+
91
+ [value, false]
92
+ end
93
+
94
+ def coerce_to_float(value)
95
+ return [value, false] unless @coercions.include?(:float)
96
+
97
+ begin
98
+ return [Float(value), true]
99
+ rescue ArgumentError # rubocop:disable Lint/SuppressedException
100
+ end
101
+
102
+ [value, false]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedSettings
4
+ module Handlers
5
+ # Base handler for a setting source
6
+ #
7
+ # All handers should inherit from this base class and implement the method
8
+ # def get(key, case_sensitive: nil)
9
+ class Base
10
+ KEY_NESTING_SEPARATOR = '.'
11
+
12
+ # rubocop:disable Lint/UnusedMethodArgument
13
+ def get(key, case_sensitive: nil)
14
+ raise 'Needs to be implemented by subclasss'
15
+ end
16
+ # rubocop:enable Lint/UnusedMethodArgument
17
+
18
+ protected
19
+
20
+ def split(val, separator)
21
+ case val
22
+ when String
23
+ val.split(separator)
24
+ when Symbol
25
+ val.to_s.split(separator)
26
+ when Array
27
+ val
28
+ else
29
+ raise 'key must either be a string or an array'
30
+ end
31
+ end
32
+
33
+ def case_sensitive?(case_sensitive)
34
+ if case_sensitive.nil?
35
+ UnifiedSettings.config.case_sensitive
36
+ else
37
+ case_sensitive
38
+ end
39
+ end
40
+
41
+ def to_symbol_array(val, separator: KEY_NESTING_SEPARATOR)
42
+ split(val, separator).map(&:to_sym)
43
+ end
44
+
45
+ def nested_key_exists?(hash, keys)
46
+ current_level = hash
47
+ keys.each do |key|
48
+ return false if current_level.nil?
49
+ return true if current_level.key?(key)
50
+
51
+ current_level = current_level[key]
52
+ end
53
+ false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module UnifiedSettings
6
+ module Handlers
7
+ # Setting handler for Config gem
8
+ class ConfigGem < Base
9
+ # By default the gem makes a `Settings` object available
10
+ DEFAULT_CONST_NAME = 'Settings'
11
+
12
+ def initialize(const_name: nil)
13
+ super()
14
+ @const_name = const_name || DEFAULT_CONST_NAME
15
+ end
16
+
17
+ def defined?(key, case_sensitive: nil)
18
+ key_arr = to_symbol_array(key)
19
+ case_sensitive = case_sensitive?(case_sensitive)
20
+
21
+ return true if nested_key_exists?(setting_obj, key_arr)
22
+ return false if case_sensitive
23
+
24
+ return true if nested_key_exists?(setting_obj, key_arr.map(&:upcase))
25
+ return true if nested_key_exists?(setting_obj, key_arr.map(&:downcase))
26
+
27
+ false
28
+ end
29
+
30
+ def get(key, case_sensitive: nil)
31
+ key_arr = to_symbol_array(key)
32
+ case_sensitive = case_sensitive?(case_sensitive)
33
+
34
+ val = setting_obj.dig(*key_arr)
35
+ return val if val
36
+ return nil if case_sensitive
37
+
38
+ val = setting_obj.dig(*key_arr.map(&:downcase))
39
+ return val if val
40
+
41
+ setting_obj.dig(*key_arr.map(&:upcase))
42
+ end
43
+
44
+ private
45
+
46
+ def setting_obj
47
+ Object.const_get("::#{@const_name}")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module UnifiedSettings
6
+ module Handlers
7
+ # Setting handler for Ruby constants
8
+ class Constants < Base
9
+ CONSTANT_KEY_NESTING_SEPARATOR = '::'
10
+
11
+ def defined?(key, case_sensitive: nil)
12
+ klass, variable_names = key_to_class_and_variable(
13
+ key, case_sensitive:
14
+ )
15
+
16
+ variable_names.each do |name|
17
+ if klass
18
+ return true if klass.const_get(name)
19
+ elsif Object.const_get(name)
20
+ return true
21
+ end
22
+ rescue NameError
23
+ # Ignore if the constant is not defined
24
+ end
25
+
26
+ false
27
+ end
28
+
29
+ def get(key, case_sensitive: nil)
30
+ klass, variable_names = key_to_class_and_variable(
31
+ key, case_sensitive:
32
+ )
33
+
34
+ variable_names.each do |name|
35
+ return klass.const_get(name) if klass
36
+
37
+ return Object.const_get(name)
38
+ rescue NameError
39
+ # Ignore if the constant is not defined
40
+ end
41
+
42
+ nil
43
+ end
44
+
45
+ private
46
+
47
+ def key_to_class_and_variable(key, case_sensitive: nil)
48
+ key_arr = to_symbol_array(key,
49
+ separator: CONSTANT_KEY_NESTING_SEPARATOR)
50
+ case_sensitive = case_sensitive?(case_sensitive)
51
+
52
+ klass, variable =
53
+ if key_arr.length > 1
54
+ [
55
+ key_arr[0..-2].join(CONSTANT_KEY_NESTING_SEPARATOR)
56
+ .safe_constantize,
57
+ key_arr[-1]
58
+ ]
59
+ else
60
+ [nil, key_arr[0]]
61
+ end
62
+
63
+ variable_names = if case_sensitive
64
+ [variable]
65
+ else
66
+ [variable, variable.upcase, variable.downcase]
67
+
68
+ end
69
+
70
+ [klass, variable_names]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module UnifiedSettings
6
+ module Handlers
7
+ # Setting handler for Rails.application.credentials
8
+ class Credentials < Base
9
+ def defined?(key, case_sensitive: nil)
10
+ key_arr = to_symbol_array(key)
11
+ case_sensitive = case_sensitive?(case_sensitive)
12
+
13
+ return true if nested_key_exists?(Rails.application.credentials,
14
+ key_arr)
15
+ return false if case_sensitive
16
+
17
+ return true if nested_key_exists?(
18
+ Rails.application.credentials, key_arr.map(&:upcase)
19
+ )
20
+ return true if nested_key_exists?(
21
+ Rails.application.credentials, key_arr.map(&:downcase)
22
+ )
23
+
24
+ false
25
+ end
26
+
27
+ def get(key, case_sensitive: nil)
28
+ key_arr = to_symbol_array(key)
29
+ case_sensitive = case_sensitive?(case_sensitive)
30
+
31
+ val = Rails.application.credentials.dig(*key_arr)
32
+ return val if val
33
+ return nil if case_sensitive
34
+
35
+ val = Rails.application.credentials.dig(*key_arr.map(&:downcase))
36
+ return val if val
37
+
38
+ Rails.application.credentials.dig(*key_arr.map(&:upcase))
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module UnifiedSettings
6
+ module Handlers
7
+ # Settings handler for ENV variables
8
+ class Env < Base
9
+ ENV_KEY_NESTING_SEPARATOR = '__'
10
+
11
+ def defined?(key, case_sensitive: nil)
12
+ key_arr = to_symbol_array(key)
13
+ case_sensitive = case_sensitive?(case_sensitive)
14
+
15
+ return true if ENV.key?(key_arr.join(ENV_KEY_NESTING_SEPARATOR))
16
+ return false if case_sensitive
17
+
18
+ return true if ENV.key?(
19
+ key_arr.map(&:upcase).join(ENV_KEY_NESTING_SEPARATOR)
20
+ )
21
+ return true if ENV.key?(
22
+ key_arr.map(&:downcase).join(ENV_KEY_NESTING_SEPARATOR)
23
+ )
24
+
25
+ false
26
+ end
27
+
28
+ def get(key, case_sensitive: nil)
29
+ key_arr = to_symbol_array(key)
30
+ case_sensitive = case_sensitive?(case_sensitive)
31
+
32
+ val = ENV.fetch(key_arr.join(ENV_KEY_NESTING_SEPARATOR), nil)
33
+ return val if val
34
+ return nil if case_sensitive
35
+
36
+ val = ENV.fetch(
37
+ key_arr.map(&:upcase).join(ENV_KEY_NESTING_SEPARATOR), nil
38
+ )
39
+ return val if val
40
+
41
+ ENV.fetch(
42
+ key_arr.map(&:downcase).join(ENV_KEY_NESTING_SEPARATOR), nil
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedSettings
4
+ # Settings
5
+ #
6
+ # Main interface for getting any of the settings
7
+ class Settings
8
+ attr_accessor :handlers
9
+
10
+ def initialize(handlers: nil)
11
+ handlers_config = handlers || UnifiedSettings.config.handlers
12
+ @handlers = handlers_config.map { |config| initialize_handler(config) }
13
+ @coercer = Coercer.new(
14
+ coercions: UnifiedSettings.config.coercions,
15
+ coerce_arrays: UnifiedSettings.config.coerce_arrays,
16
+ array_separator: UnifiedSettings.config.coerce_array_separator
17
+ )
18
+ end
19
+
20
+ def defined?(key, case_sensitive: nil)
21
+ return false unless key
22
+
23
+ @handlers.each do |handler|
24
+ return true if handler.defined?(key, case_sensitive:)
25
+ end
26
+
27
+ false
28
+ end
29
+
30
+ def get(
31
+ key, default: NO_DEFAULT, case_sensitive: nil, coerce: true,
32
+ on_missing_key: nil
33
+ )
34
+ return nil unless key
35
+
36
+ @handlers.each do |handler|
37
+ val = handler.get(key, case_sensitive:)
38
+ unless val.nil?
39
+ return coerce ? @coercer.coerce(val) : val
40
+ end
41
+ end
42
+
43
+ handle_missing_key(key, default:, on_missing_key:)
44
+ end
45
+
46
+ private
47
+
48
+ def initialize_handler(config)
49
+ handler, params = if config.respond_to?(:keys)
50
+ [config[:handler], config[:params]]
51
+ else
52
+ [config, nil]
53
+ end
54
+
55
+ case handler
56
+ when String
57
+ klass = handler.safe_constantize
58
+ params.blank? ? klass.new : klass.new(**params)
59
+ when Class
60
+ params.blank? ? handler.new : handler.new(**params)
61
+ when SettingHandler
62
+ handler
63
+ else
64
+ raise 'UnifiedSettings: Unsupported handler. Handlers must be an ' \
65
+ 'array of strings, classes, or instances of ' \
66
+ 'UnifiedSettings::SettingHandler'
67
+ end
68
+ end
69
+
70
+ def handle_missing_key(key, default: NO_DEFAULT, on_missing_key: nil)
71
+ handle_on_missing_key(key, on_missing_key:)
72
+
73
+ val = if default == NO_DEFAULT
74
+ UnifiedSettings.config.default_value
75
+ else
76
+ default
77
+ end
78
+ return val.call(key) if val.respond_to?(:call)
79
+
80
+ val
81
+ end
82
+
83
+ # rubocop:disable Metrics/CyclomaticComplexity
84
+ def handle_on_missing_key(key, on_missing_key: nil)
85
+ actions = on_missing_key || UnifiedSettings.config.on_missing_key
86
+ actions = [actions] unless actions.is_a?(Array)
87
+
88
+ actions.each do |action|
89
+ if action.respond_to?(:call)
90
+ action.call(key)
91
+ next
92
+ end
93
+
94
+ case action
95
+ when :raise
96
+ on_missing_key_raise(key)
97
+ when :log_debug
98
+ on_missing_key_log(Logger::DEBUG, key)
99
+ when :log_info
100
+ on_missing_key_log(Logger::INFO, key)
101
+ when :log_warn
102
+ on_missing_key_log(Logger::WARN, key)
103
+ when :log_error
104
+ on_missing_key_log(Logger::ERROR, key)
105
+ when :log_fatal
106
+ on_missing_key_log(Logger::FATAL, key)
107
+ else
108
+ raise "UnifiedSettings: Unknown on_missing_key handler: '#{action}'"
109
+ end
110
+ end
111
+ end
112
+ # rubocop:enable Metrics/CyclomaticComplexity
113
+
114
+ def on_missing_key_raise(key)
115
+ raise error_message(key)
116
+ end
117
+
118
+ def on_missing_key_log(level, key)
119
+ ::Rails.logger.add(level) { error_message(key) }
120
+ rescue NoMethodError
121
+ logger = Logger.new($stdout, Logger::INFO)
122
+ logger.add(level) { error_message(key) }
123
+ end
124
+
125
+ def error_message(key)
126
+ "UnifiedSettings: No matches found for '#{key}'"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # Unified way to get settings
6
+ module UnifiedSettings
7
+ include ActiveSupport::Configurable
8
+
9
+ NO_DEFAULT = :no_default
10
+
11
+ def self.configure
12
+ # Set the defaults
13
+ config.handlers = [
14
+ Handlers::Env, Handlers::Credentials, Handlers::Constants
15
+ ]
16
+ config.default_value = nil
17
+ config.case_sensitive = false
18
+ config.on_missing_key = [:log_error]
19
+ config.coercions = %i[nil boolean integer float]
20
+ config.coerce_arrays = true
21
+ config.coerce_array_separator = ','
22
+
23
+ super
24
+
25
+ # Create an instance of the settings that can be used
26
+ @settings = Settings.new
27
+ end
28
+
29
+ def self.defined?(key, case_sensitive: nil, handlers: nil)
30
+ settings = handlers.nil? ? @settings : Settings.new(handlers:)
31
+ settings.defined?(key, case_sensitive:)
32
+ end
33
+
34
+ # rubocop:disable Metrics/ParameterLists
35
+ def self.get(
36
+ key, default: NO_DEFAULT, case_sensitive: nil, handlers: nil, coerce: true,
37
+ on_missing_key: nil
38
+ )
39
+ settings = handlers.nil? ? @settings : Settings.new(handlers:)
40
+ settings.get(
41
+ key,
42
+ case_sensitive:,
43
+ coerce:,
44
+ on_missing_key:,
45
+ default:
46
+ )
47
+ end
48
+ # rubocop:enable Metrics/ParameterLists
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedSettings
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unified_settings/version'
4
+
5
+ directories = [
6
+ File.join(File.dirname(__FILE__), 'unified_settings'),
7
+ File.join(File.dirname(__FILE__), 'unified_settings', 'handlers')
8
+ ]
9
+
10
+ directories.each do |directory|
11
+ Dir[File.join(directory, '*.rb')].each do |file|
12
+ require file
13
+ end
14
+ end
15
+
16
+ ActiveRecord::Base.instance_eval { include UnifiedSettings }
17
+ if defined?(Rails) && Rails.version.to_i < 4
18
+ raise 'This version of unified_settings requires Rails 4 or higher'
19
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'unified_settings/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'unified_settings'
9
+ spec.version = UnifiedSettings::VERSION
10
+ spec.authors = ['Patrick R. Schmid']
11
+ spec.email = ['prschmid@gmail.com']
12
+
13
+ spec.summary = 'A unified way to get settings from different sources.'
14
+ spec.description = 'A unified way to get settings from different sources.'
15
+ spec.homepage = 'https://github.com/prschmid/unified_settings'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
19
+ # 'allowed_push_host' to allow pushing to a single host or delete this
20
+ # section to allow pushing to any host.
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata['homepage_uri'] = spec.homepage
23
+ spec.metadata['source_code_uri'] = 'https://github.com/prschmid/unified_settings'
24
+ spec.metadata['changelog_uri'] = 'https://github.com/prschmid/unified_settings'
25
+ else
26
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
27
+ 'public gem pushes.'
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(__dir__) do
33
+ `git ls-files -z`.split("\x0").reject do |f|
34
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
35
+ end
36
+ end
37
+ spec.bindir = 'exe'
38
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ['lib']
40
+
41
+ spec.required_ruby_version = '>= 3.2'
42
+
43
+ spec.add_runtime_dependency('activerecord', '> 4.2.0')
44
+ spec.add_runtime_dependency('activesupport', '> 4.2.0')
45
+ spec.metadata['rubygems_mfa_required'] = 'true'
46
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unified_settings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Patrick R. Schmid
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 4.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 4.2.0
41
+ description: A unified way to get settings from different sources.
42
+ email:
43
+ - prschmid@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rubocop.yml"
49
+ - ".ruby-gemset"
50
+ - ".ruby-version"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - lib/unified_settings.rb
57
+ - lib/unified_settings/coercer.rb
58
+ - lib/unified_settings/handlers/base.rb
59
+ - lib/unified_settings/handlers/config_gem.rb
60
+ - lib/unified_settings/handlers/constants.rb
61
+ - lib/unified_settings/handlers/credentials.rb
62
+ - lib/unified_settings/handlers/env.rb
63
+ - lib/unified_settings/settings.rb
64
+ - lib/unified_settings/unified_settings.rb
65
+ - lib/unified_settings/version.rb
66
+ - unified_settings.gemspec
67
+ homepage: https://github.com/prschmid/unified_settings
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/prschmid/unified_settings
72
+ source_code_uri: https://github.com/prschmid/unified_settings
73
+ changelog_uri: https://github.com/prschmid/unified_settings
74
+ rubygems_mfa_required: 'true'
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3.2'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.4.10
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: A unified way to get settings from different sources.
94
+ test_files: []