loadout 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b53dcf7016fe7d052fa02cfa1b4f7afba43f1deb7fc3bb4dcd0a87d065394174
4
+ data.tar.gz: 656914809769c23d3d4822aca33eb8e276c8cfbda0bf409aa64d9ea87c3223df
5
+ SHA512:
6
+ metadata.gz: ab1768597fa1d238cdfb15f36c3358dec842d4a8f045e1b0d87ff3ce71a50984ffb53ce8364d6ab432a53e6028e3e0f8d0693325e3094fcacd6a1912c027eb5c
7
+ data.tar.gz: 5b8ca42f8a44182e9007c3372f9ac470d0abd632d202dd54da71f22bf909a6b00def14b97c13d6d917b308c6123fde5bfd959b8d9226a953e6ae911f57df037c
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.0.7
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-08-11
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Max Chernyak
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,227 @@
1
+ # Loadout
2
+
3
+ Rails vanilla config is good enough, but tends to get messy. This gem provides a few helpers to
4
+
5
+ - Reduce repetition
6
+ - Raise error when required ENV vars or credentials are unset
7
+ - Parse reasonable ENV values into booleans, integers, floats, and arrays
8
+
9
+ ## Synopsis
10
+
11
+ ```ruby
12
+ Rails.application.configure do
13
+ extend Loadout::Helpers
14
+
15
+ config.some_secret = cred(:secret) { 'default' }
16
+ config.value_from_env_or_cred = env.cred(:key_name)
17
+
18
+ prefix(:service) do
19
+ config.x.service.optional_string = env.cred(:api_key) { 'default_key' }
20
+ config.x.service.required_string = env.cred(:api_secret)
21
+
22
+ config.x.service.optional_bool = bool.env(:bool_flag) { false }
23
+ config.x.service.number = int.env.cred(:number) { nil }
24
+ config.x.service.float = float.env.cred(:number)
25
+ config.x.service.array = list.env(:comma_list)
26
+ end
27
+ end
28
+ ```
29
+
30
+ ## Installation
31
+
32
+ Note: this gem requires Ruby 3.
33
+
34
+ Install the gem and add to the application's Gemfile by executing:
35
+
36
+ $ bundle add loadout
37
+
38
+ If bundler is not being used to manage dependencies, install the gem by executing:
39
+
40
+ $ gem install loadout
41
+
42
+ ## Usage
43
+
44
+ 1. Include helpers into your `config/application.rb` and `config/environments/*.rb`:
45
+
46
+ ```ruby
47
+ extend Loadout::Helpers
48
+ ```
49
+
50
+ This should be done in each file where you'd like to use loadout.
51
+
52
+ 2. Grab a value from credentials:
53
+
54
+ ```ruby
55
+ config.key = cred(:key_name)
56
+ ```
57
+
58
+ 3. Or from ENV:
59
+
60
+ ```ruby
61
+ config.key = env(:key_name)
62
+ ```
63
+
64
+ 4. Or whichever one is found first:
65
+
66
+ ```ruby
67
+ config.key = env.cred(:key_name)
68
+ ```
69
+
70
+ 5. Or the other way around:
71
+
72
+ ```ruby
73
+ config.key = cred.env(:key_name)
74
+ ```
75
+
76
+ 6. If it's a nested credential value, you can supply multiple keys:
77
+
78
+ ```ruby
79
+ # Look up service.key_name in credentials
80
+ config.key = cred(:service, :key_name)
81
+ ```
82
+
83
+ 7. It will do the right thing if you also add env:
84
+
85
+ ```ruby
86
+ # Look up service.key_name in credentials, or SERVICE_KEY_NAME in ENV
87
+ config.key = cred.env(:service, :key_name)
88
+ ```
89
+
90
+ 8. Parse ENV value into a boolean:
91
+
92
+ ```ruby
93
+ # Valid true strings: 1/y/yes/t/true
94
+ # Valid false strings: "" or 0/n/no/f/false
95
+ # (case insensitive)
96
+ #
97
+ # Any other string will raise an error.
98
+ config.some_flag = bool.cred.env(:key_name)
99
+ ```
100
+
101
+ Note: because credentials come from YAML, they don't need to be parsed. Only ENV values are parsed.
102
+
103
+ 9. Integers and floats are also supported:
104
+
105
+ ```ruby
106
+ config.some_int = int.cred.env(:int_key_name)
107
+ config.some_float = float.cred.env(:float_key_name)
108
+ ```
109
+
110
+ 10. Lists are supported too:
111
+
112
+ ```ruby
113
+ # Parses strings like "foo, bar, baz", "foo|bar|baz", "foo bar baz" into ['foo', 'bar', 'baz']
114
+ config.some_list = list.cred.env(:key_name)
115
+ ```
116
+
117
+ 11. You can set your own list separator (string or regex):
118
+
119
+ ```ruby
120
+ # Parses 'foo0bar0baz' as ['foo', 'bar', 'baz']
121
+ config.some_list = list('0').env(:key_name)
122
+ ```
123
+
124
+ 12. Use a block at the end to specify a default value:
125
+
126
+ ```ruby
127
+ config.some_list = list.cred.env(:key_name) { ['default'] }
128
+ ```
129
+
130
+ 13. Use prefix to avoid repeating the same nesting:
131
+
132
+ ```ruby
133
+ prefix(:service) do
134
+ config.x.service.api_key = env(:api_key) # Looks up "SERVICE_API_KEY"
135
+ config.x.service.api_secret = env(:api_secret) # Looks up "SERVICE_API_SECRET"
136
+ end
137
+ ```
138
+
139
+ Note that left hand side is unaffected. Only loadout helpers get auto-prefixed.
140
+
141
+ 14. If you'd like a way to shorten the left hand side too, you can assign the whole group as a hash or OrderedOptions (this is not a loadout feature, just something you can do with Rails):
142
+
143
+ ```ruby
144
+ prefix(:service) do
145
+ config.x.service = ActiveSupport::OrderedOptions[
146
+ api_key: env(:api_key),
147
+ api_secret: env(:api_secret)
148
+ ]
149
+ end
150
+ ```
151
+
152
+ 15. Since `prefix` returns the block's result, you can rewrite the above as follows:
153
+
154
+ ```ruby
155
+ config.x.service = prefix(:service) {
156
+ ActiveSupport::OrderedOptions[
157
+ api_key: env(:api_key),
158
+ api_secret: env(:api_secret)
159
+ ]
160
+ }
161
+ ```
162
+
163
+ 16. `prefix` lets you supply a default to the whole block:
164
+
165
+ ```ruby
166
+ prefix(:service, default: -> { 'SECRET' }) do
167
+ config.x.service.api_key = env(:api_key) # falls back to 'SECRET'
168
+ config.x.service.api_secret = env(:api_secret) # falls back to 'SECRET'
169
+ end
170
+ ```
171
+
172
+ ## Advanced configuration
173
+
174
+ ### I don't like all these helpers polluting my config!
175
+
176
+ Instead of `extend Loadout::Helpers` you can `extend Loadout` to include one proxy method `loadout`. Now all helpers live in one place.
177
+
178
+ ```ruby
179
+ Rails.application.configure do
180
+ extend Loadout
181
+
182
+ config.some_key = loadout.cred.env(:some_key)
183
+ end
184
+ ```
185
+
186
+ Feel free to alias it to something shorter if you'd like:
187
+
188
+ ```ruby
189
+ Rails.application.configure do
190
+ extend Loadout
191
+ alias l loadout
192
+
193
+ config.some_key = l.cred.env(:some_key)
194
+ end
195
+ ```
196
+
197
+ ### Credentials and ENV
198
+
199
+ By default loadout will look into `credentials` and `ENV` in your config's context. If your credentials are called something else, or you want to supply an alternative source of ENV, you can configure it like so:
200
+
201
+ ```ruby
202
+ Rails.application.configure do
203
+ extend Loadout::Helpers
204
+ loadout creds: alt_credentials, env: alt_env
205
+
206
+ # Now loadout will use alt_credentials and alt_env to look up values.
207
+ end
208
+ ```
209
+
210
+
211
+ ## Development
212
+
213
+ 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.
214
+
215
+ 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).
216
+
217
+ ## Contributing
218
+
219
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/loadout. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/loadout/blob/main/CODE_OF_CONDUCT.md).
220
+
221
+ ## License
222
+
223
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
224
+
225
+ ## Code of Conduct
226
+
227
+ Everyone interacting in the Loadout project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/loadout/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'minitest/test_task'
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Loadout
4
+ VERSION = '0.1.0'
5
+ end
data/lib/loadout.rb ADDED
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'loadout/version'
4
+ require 'set'
5
+
6
+ module Loadout
7
+ NONE = BasicObject.new
8
+ DEFAULT_LIST_SEP = /\s*[\s[[:punct:]]]+\s*/
9
+
10
+ ConfigError = Class.new(ArgumentError)
11
+ MissingConfigError = Class.new(ConfigError)
12
+ InvalidConfigError = Class.new(ConfigError)
13
+
14
+ def loadout(env: nil, creds: nil)
15
+ @loadout ||= Loadout::Config.new(env || ENV, creds || credentials)
16
+ end
17
+
18
+ module Helpers
19
+ def loadout(env: nil, creds: nil)
20
+ @loadout ||= Loadout::Config.new(env || ENV, creds || credentials)
21
+ end
22
+
23
+ def cred(*a, **k, &b) = loadout.cred(*a, **k, &b)
24
+ def env(*a, **k, &b) = loadout.env(*a, **k, &b)
25
+ def prefix(*a, **k, &b) = loadout.prefix(*a, **k, &b)
26
+
27
+ def bool(*a, **k, &b) = loadout.bool(*a, **k, &b)
28
+ def int(*a, **k, &b) = loadout.int(*a, **k, &b)
29
+ def float(*a, **k, &b) = loadout.float(*a, **k, &b)
30
+ def list(*a, **k, &b) = loadout.list(*a, **k, &b)
31
+ end
32
+
33
+ class Config
34
+ protected attr_writer :type
35
+ protected attr_reader :lookup_stack
36
+
37
+ def initialize(env, creds)
38
+ @env = env
39
+ @creds = creds
40
+ @type = nil
41
+ @prefix_stack = []
42
+ @lookup_stack = Set[]
43
+ @prefix_default = NONE
44
+ end
45
+
46
+ def env(*keys, &default)
47
+ return dup.tap { _1.lookup_stack << :env } if keys.empty?
48
+ @lookup_stack << :env
49
+ lookup(keys, &default)
50
+ end
51
+
52
+ def cred(*keys, &default)
53
+ return dup.tap { _1.lookup_stack << :cred } if keys.empty?
54
+ @lookup_stack << :cred
55
+ lookup(keys, &default)
56
+ end
57
+
58
+ def prefix(*keys, default: NONE)
59
+ @prefix_default = default unless default.equal?(NONE)
60
+ @prefix_stack.push(keys)
61
+ yield.tap { @prefix_stack.pop }
62
+ end
63
+
64
+ def bool = dup.tap { _1.type = :bool }
65
+ def int = dup.tap { _1.type = :int }
66
+ def float = dup.tap { _1.type = :float }
67
+ def list(sep = DEFAULT_LIST_SEP) = dup.tap { _1.type = [:list, sep] }
68
+
69
+ def initialize_dup(other)
70
+ @creds = other.instance_variable_get(:@creds)
71
+ @type = other.instance_variable_get(:@type).dup
72
+ @prefix_stack = other.instance_variable_get(:@prefix_stack).dup
73
+ @lookup_stack = other.instance_variable_get(:@lookup_stack).dup
74
+
75
+ unless other.instance_variable_get(:@prefix_default).equal?(NONE)
76
+ @prefix_default = other.instance_variable_get(:@prefix_default).dup
77
+ end
78
+
79
+ super
80
+ end
81
+
82
+ private
83
+
84
+ def lookup(keys)
85
+ value = NONE
86
+ keys = @prefix_stack.flatten + keys
87
+
88
+ @lookup_stack.each do |source|
89
+ value =
90
+ case source
91
+ when :cred; lookup_cred(keys)
92
+ when :env; lookup_env(keys)
93
+ end
94
+
95
+ return value unless value.equal?(NONE)
96
+ end
97
+
98
+ return yield if block_given?
99
+ return @prefix_default.call unless @prefix_default.equal?(NONE)
100
+ raise_missing(keys)
101
+ ensure
102
+ @lookup_stack.clear
103
+ end
104
+
105
+ def lookup_cred(keys)
106
+ return @creds[keys[0]] if keys.one? && @creds.has_key?(keys[0])
107
+ return NONE if keys.one?
108
+ hash = @creds.dig(*keys[..-2])
109
+ hash&.has_key?(keys.last) ? hash[keys.last] : NONE
110
+ end
111
+
112
+ def lookup_env(keys)
113
+ env_key = keys.join('_').upcase
114
+ @env.has_key?(env_key) ? coerce(env_key, @env[env_key]) : NONE
115
+ end
116
+
117
+ def coerce(key, value)
118
+ case @type
119
+ in :bool
120
+ value = value.to_s
121
+ return false if value == ''
122
+ return false if %w[0 n no f false].include?(value.downcase)
123
+ return true if %w[1 y yes t true].include?(value.downcase)
124
+ raise_invalid :bool, key, value
125
+ in :int; enhance_exception(:int, key, value) { Integer(value) }
126
+ in :float; enhance_exception(:float, key, value) { Float(value) }
127
+ in :list, sep; value.split(sep)
128
+ else; value
129
+ end
130
+ end
131
+
132
+ def enhance_exception(type, key, val)
133
+ yield
134
+ rescue ArgumentError
135
+ raise_invalid(type, key, val)
136
+ end
137
+
138
+ def raise_missing(keys)
139
+ src = []
140
+ val = []
141
+
142
+ @lookup_stack.each do |source|
143
+ case source
144
+ when :cred
145
+ src << "credential"
146
+ val << keys.join('.')
147
+ when :env
148
+ src << "environment variable"
149
+ val << keys.join('_').upcase
150
+ end
151
+ end
152
+
153
+ msg = src.zip(val).map { |s, v| "#{s} (#{v})" }.join(' or ')
154
+ raise MissingConfigError, "required #{msg} is not set"
155
+ end
156
+
157
+ def raise_invalid(type, key, val)
158
+ raise InvalidConfigError, "invalid value for #{type} (`#{val}`) in #{key}"
159
+ end
160
+ end
161
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: loadout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Max Chernyak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A few helpers to make vanilla Rails configuration neater.
14
+ email:
15
+ - hello@max.engineer
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".tool-versions"
21
+ - CHANGELOG.md
22
+ - CODE_OF_CONDUCT.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/loadout.rb
27
+ - lib/loadout/version.rb
28
+ homepage: https://github.com/maxim/loadout
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ allowed_push_host: https://rubygems.org
33
+ homepage_uri: https://github.com/maxim/loadout
34
+ source_code_uri: https://github.com/maxim/loadout
35
+ changelog_uri: https://github.com/maxim/loadout/blob/main/CHANGELOG.md
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 3.0.0
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.2.33
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Rails configuration helpers
55
+ test_files: []