fat_config 0.4.1

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: e6a9ee861c75317e6c334ccb6639eda669b794a3f7a6e33543e91cf9193d5fd1
4
+ data.tar.gz: 760c48576a7d7d2376a3524fc9301f0593261a9e6a9ebd75f431683e9e56b9bf
5
+ SHA512:
6
+ metadata.gz: a111ad1acbdda7c5070910a1b78c3c9ab389b4ce45012a97642071d6fa7db21550b7806388b31827db0fbdda62ee16373550f0282daf20d68bfe5180ca9405a9
7
+ data.tar.gz: 64ee672822816a6ff7776537817ad2bdaa73c7b446ab86c50954ba38ee408f676006957734996a5ec454ade4c5ebdedd00fd274befb30bf2457751f5397226d9
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require 'spec_helper'
2
+ --color
3
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ inherit_from: ./rubocop-global.yml
2
+
3
+ require:
4
+ - rubocop-rspec
5
+ - rubocop-rake
6
+ - rubocop-obsession
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 3.3
10
+ Include:
11
+ - 'lib/**/*'
12
+ - 'bin/**/*'
13
+ - 'spec/**/*'
14
+ # - 'features/**/*'
15
+ Exclude:
16
+ - 'spec/tmp/**/*'
17
+ - 'spec/.examples.txt'
18
+ - 'bin/setup'
19
+ - '.simplecov'
20
+ - 'features/**/*'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Daniel E. Doherty
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.org ADDED
@@ -0,0 +1,315 @@
1
+ # FatConfig
2
+
3
+ ~FatConfig~ eliminates the tedium of reading configuration files and the
4
+ environment to populate a Hash of configuration settings. You need only
5
+ define a ~FatConfig::Reader~ and you can call its ~#read~ method to look for,
6
+ read, translate, and merge any config files into a single Hash that
7
+ encapsulates all the files in the proper priority. It can be set to read
8
+ ~YAML~, ~TOML~, ~JSON~, or ~INI~ config files.
9
+
10
+ * Installation
11
+
12
+ Install the gem and add to the application's Gemfile by executing:
13
+
14
+ #+begin_src sh
15
+ bundle add fat_config
16
+ #+end_src
17
+
18
+ If bundler is not being used to manage dependencies, install the gem by executing:
19
+
20
+ #+begin_src sh
21
+ gem install fat_config
22
+ #+end_src
23
+
24
+ * Usage:
25
+
26
+ #+begin_src ruby
27
+ require 'fat_config'
28
+
29
+ reader = FatConfig::Reader.new('myapp')
30
+ config = reader.read
31
+ #+end_src
32
+
33
+ The ~reader.read~ method will parse the config files (by default assumed to be
34
+ YAML files), config environment variable, and optional command-line parameters
35
+ and return the composite config as a Hash.
36
+
37
+ ** Following XDG Standards
38
+ By default, ~FatConfig::Reader#read~ follows the [[https://specifications.freedesktop.org/basedir-spec/latest/][XDG Desktop Standards]],
39
+ reading configuration settings for a hypothetical application called ~myapp~
40
+ from the following locations, from lowest priority to highest:
41
+
42
+ 1. If the environment variable ~MYAPP_SYS_CONFIG~ is set to the name of a
43
+ file, it will look in that file for any system-level config file.
44
+ 2. If the environment variable ~MYAPP_SYS_CONFIG~ is NOT set, it will read any
45
+ system-level config file from ~/etc/xdg/myapp~ or, if the ~XDG_CONFIG_DIRS~
46
+ environment variable is set to a list of colon-separated directories, it
47
+ will look in each of those instead of ~/etc/xdg~ for config directories
48
+ called ~myapp~. If more than one ~XDG_CONFIG_DIRS~ is given, they are
49
+ treated as listed in order of precedence, so the first-listed directory
50
+ will be given priority over later ones. All such directories will be read,
51
+ and any config file found will be merged into the resulting Hash, but they
52
+ will be visited in reverse order so that the first-named directories
53
+ override the earlier ones.
54
+ 3. If the environment variable ~MYAPP_CONFIG~ is set to a file name, it will
55
+ look in that file any user-level config file.
56
+ 4. If the environment variable ~MYAPP_CONFIG~ is NOT set, it will read any
57
+ user-level config file from ~$HOME/.config/myapp~ or, if the
58
+ ~XDG_CONFIG_HOME~ environment variable is set to an alternative directory,
59
+ it will look ~XDG_CONFIG_HOME/.config~ for a config directory called
60
+ 'myapp'. Note that in this case, ~XDG_CONFIG_HOME~ is intended to contain
61
+ the name of a single directory, not a list of directories as with the
62
+ system-level config files.
63
+ 5. It will then merge in any options set in the environment variable
64
+ ~MYAPP_OPTIONS~, overriding any conflicting settings gotten from reading
65
+ the system- and user-level file. It will interpret the String from the
66
+ environment variable as discussed below in [[*Parsing Environment and Command Line Strings][Parsing Environment and Command
67
+ Line Strings]].
68
+ 6. Finally, it will merge in any options given in the optional ~command_line:~
69
+ named parameter to the ~#read~ method. That parameter can either be a
70
+ ~Hash~ or a ~String~. If it is a ~String~, it is interpreted the same way
71
+ as the environment variable ~MYAPP_OPTIONS~ as explained below in [[*Parsing Environment and Command Line Strings][Parsing
72
+ Environment and Command Line Strings]]; if it is a ~Hash~, it is used
73
+ directly and merged into the hash returned from the prior methods.
74
+
75
+ ** Following Classic UNIX Standards
76
+ With the optional ~:xdg~ keyword parameter to ~FatConfig::Reader#read~ set to
77
+ ~false~, it will follow "classic" UNIX config file conventions. There is no
78
+ "standard" here, but there are some conventions, and the closest thing I can
79
+ find to describe the conventions is this from the [[https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s08.html#homeReferences][UNIX File Hierarchy Standard]]
80
+ website:
81
+
82
+ #+begin_quote
83
+ User specific configuration files for applications are stored in the user's
84
+ home directory in a file that starts with the '.' character (a "dot
85
+ file"). If an application needs to create more than one dot file then they
86
+ should be placed in a subdirectory with a name starting with a '.' character,
87
+ (a "dot directory"). In this case the configuration files should not start
88
+ with the '.' character.
89
+ #+end_quote
90
+
91
+ ~FatConfig~'s implementation of this suggestion are as follows for a
92
+ hypothetical application called ~myapp~:
93
+
94
+ 1. If the environment variable ~MYAPP_SYS_CONFIG~ is set to a file name, it
95
+ will look in that file for any system-level config file.
96
+ 2. If the environment variable ~MYAPP_SYS_CONFIG~ is NOT set, then either
97
+ - if the file ~/etc/my_app~ exists and is readable, it is considered the
98
+ system-wide config file for ~my_app~, or
99
+ - if the file ~/etc/my_apprc~ exists and is readable, it is considered the
100
+ system-wide config file for ~my_app~, or
101
+ - if the /directory/ ~/etc/my_app~ exists, the first file named ~config~,
102
+ ~config.yml~, ~config.yaml~ (this assumes the default YAML style, the
103
+ extensions looked for will be adjusted for other styles) ,
104
+ ~myapp.config~, or ~myapp.cfg~ that is readable will be considered the
105
+ system-wide config file for ~my_app~
106
+ 3. If the environment variable ~MYAPP_CONFIG~ is set to a file name, it will
107
+ look in that file for any user-level config file.
108
+ 4. If the environment variable ~MYAPP_CONFIG~ is NOT set, then either,
109
+ - if the file, =~/.my_app= or =~/.my_apprc~= exist and are readable, that
110
+ file is used as the user-level config file,
111
+ - otherwise, if the directory =~/.my_app/= exists, the first file in that
112
+ directory named ~config~, ~config.yml~, ~config.yaml~, ~myapp.config~, or
113
+ ~myapp.cfg~ that is readable will be considered the user-level config
114
+ file for ~my_app~
115
+ 5. It will then merge in any options set in the environment variable
116
+ ~MYAPP_OPTIONS~, overriding any conflicting settings gotten from reading
117
+ the system- and user-level file. It will interpret the environment setting
118
+ as explained below in [[*Parsing Environment and Command Line Strings][Parsing Environment and Command Line Strings]].
119
+ 6. Finally, it will merge in any options given in the optional ~command_line:~
120
+ named parameter to the ~#read~ method. That parameter can either be a
121
+ ~Hash~ or a ~String~. If it is a ~String~, it will interpret the string as
122
+ explained below in [[*Parsing Environment and Command Line Strings][Parsing Environment and Command Line Strings]]; if it is a
123
+ ~Hash~, it is used directly and merged into the hash returned from the
124
+ prior methods.
125
+
126
+ ** Available Config File Styles
127
+ ~FatConfig::Reader.new~ takes the optional keyword argument, ~:style~, to
128
+ indicate what style to use for config files. It can be one of:
129
+
130
+ - ~yaml~ :: See [[https://yaml.org/spec/1.2.2/][YAML Specs]],
131
+ - ~toml~ :: See [[https://toml.io/en/][TOML Specs]],
132
+ - ~json~ :: See [[https://datatracker.ietf.org/doc/html/rfc8259][JSON Specs]], or
133
+ - ~ini~ :: See [[https://en.wikipedia.org/wiki/INI_file][INI File on Wikipedia]]
134
+
135
+ By default, the style is ~yaml~. Note that the style only pertains to the
136
+ syntax of on-disk configuration files. Configuration can also be set by an
137
+ environment variable, ~MYAPP_OPTIONS~ and by a command-line string optionally
138
+ provided to the ~#read~ method. Those are simple parsers that parse strings
139
+ of option settings as explained below. See, [[*Parsing Environment and Command Line Strings][Parsing Environment and Command
140
+ Line Strings]].
141
+
142
+ ** Hash Keys
143
+ The returned Hash will have symbols as keys, using the names given in the
144
+ config files, except that they will have any hyphens converted to the
145
+ underscore. Thus the config setting "page-width: 6.5in" in a config file will
146
+ result in a Hash entry of ~{ page_width: '6.5in' }~.
147
+
148
+ ** Hash Values
149
+ Whether the values of the returned Hash will be 'deserialized' into a Ruby
150
+ object is controlled by the style of the configuration files. For example,
151
+ the ~:yaml~ style deserializes the following types:
152
+
153
+ *** YAML
154
+
155
+ - TrueClass (the string 'true' of whatever case)
156
+ - FalseClass (the string 'false' of whatever case)
157
+ - NilClass (when no value given)
158
+ - Integer (when it looks like an whole number)
159
+ - Float (when it looks like an decimal number)
160
+ - String (if not one of the other classes or if enclosed in single- or double-quotes)
161
+ - Array (when sub-elements introduced with '-', each typed by these rules)
162
+ - Hash, (when sub-elements introduced with 'key:', each typed by these rules) and,
163
+ - Date, DateTime, and Time, which FatConfig adds to the foregoing default
164
+ types deserialized by the default YAML library.
165
+
166
+ *** TOML
167
+
168
+ - TrueClass (exactly the string 'true')
169
+ - FalseClass (exactly the string 'false')
170
+ - Integer (when it looks like an whole number or 0x... or 0o... hex or octal)
171
+ - Float (when it looks like an decimal number)
172
+ - String (only if enclosed in single- or double-quotes)
173
+ - Array (when sub-elements enclosed in [...], each typed by these rules)
174
+ - Hash, ([hash-key] followed by sub-elements, each typed by these rules) and,
175
+ - Date and Time, when given in ISO form YYYY-MM-DD or YYYY-MM-DDThh:mm:ss
176
+
177
+ *** JSON
178
+
179
+ - TrueClass (exactly the string 'true')
180
+ - FalseClass (exactly the string 'false')
181
+ - Integer (when it looks like an decimal whole number, but NO provision hex
182
+ or octal)
183
+ - Float (when it looks like an decimal number)
184
+ - String (only if enclosed in single- or double-quotes)
185
+ - Array (when sub-elements enclosed in [...], each typed by these rules)
186
+ - Hash, (when sub-elements enclosed in {...}, each typed by these rules) and,
187
+ - Date and Time, NOT deserialized, returns a parse error
188
+
189
+ *** INI
190
+
191
+ - TrueClass (exactly the string 'true')
192
+ - FalseClass (exactly the string 'false')
193
+ - Integer (when it looks like an whole number or 0x... or 0o... hex or octal)
194
+ - Float (when it looks like an decimal number)
195
+ - String (anything else)
196
+ - Array NOT deserialized, returned as a String
197
+ - Hash, NOT deserialized, returned as a String
198
+ - Date and Time, NOT deserialized, returned as a String
199
+
200
+ ** Creating a Reader
201
+ When creating a ~Reader~, the ~#new~ method takes a mandatory argument that
202
+ specifies the name of the application for which configuration files are to be
203
+ sought. It also takes a few optional keyword arguments:
204
+
205
+ - ~style:~ specify a style for the config files other than YAML, the choices
206
+ are ~yaml~, ~toml~, ~json~, and ~ini~. This can be given either as a String
207
+ or Symbol in upper or lower case.
208
+ - ~xdg:~ either ~true~, to follow the XDG standard for where to find config
209
+ files, or ~false~, to follow classic UNIX conventions.
210
+ - ~root_prefix:~, to locate the root of the file system somewhere other than
211
+ ~/~. This is probably only useful in testing ~FatConfig~.
212
+
213
+ #+begin_src ruby
214
+ require 'fat_config'
215
+
216
+ reader1 = FatConfig.new('labrat') # Use XDG and YAML
217
+ reader2 = FatConfig.new('labrat', style: 'toml') # Use XDG and TOML
218
+ reader3 = FatConfig.new('labrat', style: 'ini', xdg: false) # Use classic UNIX and INI style
219
+ #+end_src
220
+
221
+ ** Calling the ~#read~ method on a ~Reader~
222
+ Once a ~Reader~ is created, you can get the completely merged configuration as
223
+ a Hash by calling ~Reader#read~. The ~read~ method can take several
224
+ parameter:
225
+
226
+ - ~alternative base~ :: as the first positional parameter, you can give an
227
+ alternative base name to use for the config files other than the app_name
228
+ given in the ~Reader.new~ constructor. This is useful for applications that
229
+ may want to have more than one set of configuration files. If given, this
230
+ name only affects the base names of the config files, not the directory in
231
+ which they are to be sought: those always use the app name.
232
+ - ~command_line:~ :: if you want a command-line to override config values, you
233
+ can supply one as either a String or a Hash to the ~command_line:~ keyword
234
+ parameter. See below for how a String is parsed.
235
+ - ~verbose:~ :: if you set ~verbose:~ true as a keyword argument, the ~read~
236
+ method will report details of how the configuration was built on ~$stderr~.
237
+
238
+ #+begin_src ruby
239
+ require 'fat_config'
240
+
241
+ reader = FatConfig::Reader.new('labrat')
242
+ reader.read # YAML configs with basename 'labrat'; XDG conventions
243
+
244
+ # Now read another config set in directories named 'labrat' but with base
245
+ # names of 'labeldb'. Overrride any setting named fog_psi with command-line
246
+ # value, and report config build on $stderr.
247
+ reader.read('labeldb', command_line: "--fog-psi=3.41mm", verbose: true)
248
+
249
+ # Similar with a Hash for the command-line
250
+ cl = { fog_psi: '3.41mm' }
251
+ reader.read('labeldb', command_line: cl, verbose: true)
252
+ #+end_src
253
+
254
+ ** Parsing Environment and Command Line Strings
255
+ The highest priority configs are those contained in the environment variable
256
+ or in any ~command-line:~ key-word parameter given to the ~#read~ method. In
257
+ the case of the environment variable, the setting is always a String read from
258
+ the environment.
259
+
260
+ The ~command_line:~ key-word parameter can be set to either a String or a
261
+ Hash. When a Hash is provided, it is used unaltered as a config hash. When a
262
+ String is provided (and in the case of the environment variable), the string
263
+ should be something like this:
264
+
265
+ #+begin_example
266
+ --hello-thing='hello, world' --gb=goodbye world --doit --the_num=3.14159 --the-date=2024-11-27 --no-bueno --~junk
267
+ #+end_example
268
+
269
+ And it is parsed into this Hash:
270
+
271
+ #+begin_src ruby
272
+ {
273
+ :hello_thing=>"hello, world",
274
+ :gb=>"goodbye",
275
+ :doit=>true,
276
+ :the_num=>"3.14159",
277
+ :the_date=>"2024-11-27",
278
+ :bueno=>false,
279
+ :junk=>false
280
+ }
281
+ #+end_src
282
+
283
+ Here are the parsing rules:
284
+
285
+ 1. A config element is either an "option," of the form
286
+ "--<option-name>=<value>" or a "flag" of the form "--<flag-name>",
287
+ everything else is ignored.
288
+ 2. All option values are returned as String's and are not deserialized into
289
+ Ruby objects,
290
+ 3. All flags are returned as a boolean ~true~ or ~false~. If the flag name
291
+ starts with 'no', 'no-', 'no_', '!', or '~', it is set to =false= and the
292
+ option name has the negating prefix stripped; otherwise, it is set to
293
+ =true=.
294
+ 4. These rules apply regardless of style being used for config files.
295
+
296
+ * Development
297
+
298
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
299
+ run `rake spec` to run the tests. You can also run `bin/console` for an
300
+ interactive prompt that will allow you to experiment.
301
+
302
+ To install this gem onto your local machine, run `bundle exec rake
303
+ install`. To release a new version, update the version number in `version.rb`,
304
+ and then run `bundle exec rake release`, which will create a git tag for the
305
+ version, push git commits and the created tag, and push the `.gem` file to
306
+ [[https://rubygems.org][rubygems.org]].
307
+
308
+ * Contributing
309
+
310
+ Bug reports and pull requests are welcome on GitHub at
311
+ https://github.com/ddoherty03/fat_config.
312
+
313
+ * License
314
+
315
+ The gem is available as open source under the terms of the [[https://opensource.org/licenses/MIT][MIT License]].
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/TODO.org ADDED
@@ -0,0 +1,11 @@
1
+ * Programming TODOS
2
+ ** Write the README.org
3
+
4
+ * Finished
5
+ ** DONE Verbose
6
+ CLOSED: [2024-11-27 Wed 07:04]
7
+ Ensure that the verbose option provides useful feedback on how the options got to be what they end up as.
8
+ ** DONE Command-line and Environment
9
+ CLOSED: [2024-11-28 Thu 10:58]
10
+ Add a parameter to the Reader#read method to merge in command-line parameters
11
+ and environment variables.
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ # Transform hash keys to symbols suitable for calling as methods, i.e.,
5
+ # translate any hyphens to underscores. This is the form we want to keep
6
+ # config hashes in Labrat.
7
+ def methodize
8
+ new_hash = {}
9
+ each_pair do |k, v|
10
+ new_val =
11
+ if v.is_a?(Hash)
12
+ v.methodize
13
+ else
14
+ v
15
+ end
16
+ new_hash[k.to_s.tr('-', '_').to_sym] = new_val
17
+ end
18
+ new_hash
19
+ end
20
+
21
+ # Print to $stderr the changes wrought by merging new_hash into this one.
22
+ def report_merge(new_hash, indent: 2)
23
+ space = ' ' * indent
24
+ if new_hash.empty?
25
+ warn "#{space}Empty config"
26
+ return self
27
+ end
28
+
29
+ new_keys = new_hash.keys
30
+ old_keys = keys
31
+ unchanged_keys = old_keys - new_keys
32
+ added_keys = new_keys - old_keys
33
+ changed_keys = old_keys & new_keys
34
+ (keys + added_keys).sort.each do |k|
35
+ if (self[k].nil? || self[k].is_a?(Hash)) && new_hash[k].is_a?(Hash)
36
+ # Recurse if the value is a Hash
37
+ warn "#{space}Config key: #{k}:"
38
+ (self[k] || {}).report_merge(new_hash[k], indent: indent + 2)
39
+ next
40
+ end
41
+ if unchanged_keys.include?(k)
42
+ warn "#{space}Unchanged: #{k}: #{self[k]}"
43
+ elsif added_keys.include?(k)
44
+ warn "#{space}Added: #{k}: #{new_hash[k]}"
45
+ elsif changed_keys.include?(k)
46
+ if self[k] != new_hash[k]
47
+ warn "#{space}Changed: #{k}: #{self[k]} -> #{new_hash[k]}"
48
+ else
49
+ warn "#{space}Unchanged: #{k}: #{self[k]} -> #{new_hash[k]}"
50
+ end
51
+ else
52
+ raise ArgumentError, "FatConfig report_merge has unmatched key: #{k}"
53
+ end
54
+ end
55
+ self
56
+ end
57
+
58
+ require 'fat_core/string'
59
+
60
+ # Parse a string of the form "--key-one=val1 --flag --key2=val2" into a
61
+ # Hash,
62
+ #
63
+ # 1. where normal option values can be surrounded by single- or double-quotes
64
+ # if they are meant to include any spaced and
65
+ # 2. where the value of any "flag", such as --flag (with no value given) is
66
+ # set to ~true~ unless its name starts with "no" or "no_" or "!" or "~",
67
+ # then set it to false and its name is stripped of the leading negator.
68
+ #
69
+ # It also converts all the keys to symbols suitable as Ruby id's using
70
+ # Hash#methodize. It ignores anything that doesn't look like an option or
71
+ # flag.
72
+ def self.parse_opts(str)
73
+ hsh = Hash[str.scan(/--?([^=\s]+)(?:=("[^"]*"|'[^']*'|\S+))?/)]
74
+ result = {}
75
+ hsh.each_pair do |k, v|
76
+ if v.nil?
77
+ if k =~ /\A((no[-_]?)|!|~)(?<name>.*)\z/
78
+ new_key = Regexp.last_match["name"]
79
+ result[new_key] = false
80
+ else
81
+ result[k] = true
82
+ end
83
+ elsif v.match?(/\A['"].*['"]\z/)
84
+ result[k] = v.clean.sub(/\A['"]/, '').sub(/['"]\z/, '')
85
+ else
86
+ result[k] = v
87
+ end
88
+ end
89
+ result.methodize
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module FatConfig
2
+ class ParseError < StandardError; end
3
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FatConfig
4
+ # This class is responsible for finding a config files, reading them, and
5
+ # returning a Hash to reflect the configuration. We use YAML as the
6
+ # configuration format and look for the config file in the standard places.
7
+ class Reader
8
+ VALID_CONFIG_STYLES = [:yaml, :toml, :json, :ini]
9
+
10
+ # - ~app_name~ :: used to form environment variables for config locations.
11
+ # - ~style~ :: either :yaml or :toml or :json or :ini
12
+ # - ~xdg~ :: whether follow XDG desktop conventions, by default true; if
13
+ # false, use "classic" UNIX config practices with /etc/ and ~/.baserc.
14
+ # - ~root_prefix~ :: an alternate root of the assumed file system, by
15
+ # default ''. This facilitated testing.
16
+ attr_reader :app_name, :style, :root_prefix, :xdg
17
+
18
+ # Config file may be located in either the xdg locations (containing any
19
+ # variant of base: base, base.yml, or base.yaml) or in the classic
20
+ # locations (/etc/app_namerc, /etc/app_name, ~/.app_namerc~, or
21
+ # ~/.app_name/base.ext). Return a hash that reflects the merging of
22
+ # those files according to the following priorities, from highest to
23
+ # lowest:
24
+ #
25
+ # 1. Options passed in the String or Hash parameter, command_line
26
+ # 2. Options passed by an environment variable APPNAME_OPTIONS
27
+ # 3. If the xdg parameter is true:
28
+ # a. Either:
29
+ # A. The file pointed to by the environment variable APPNAME_CONFIG or
30
+ # B. User xdg config files for app_name,
31
+ # b. Then, either:
32
+ # A. The file pointed to by the environment variable APPNAME_SYS_CONFIG or
33
+ # B. System xdg config files for for app_name,
34
+ # 4. If the xdg parameter is false:
35
+ # a. Either:
36
+ # A. The file pointed to by the environment variable APPNAME_CONFIG or
37
+ # B. User classic config files
38
+ # b. Then, either:
39
+ # A. The file pointed to by the environment variable APPNAME_SYS_CONFIG or
40
+ # B. System classic config files,
41
+ def initialize(app_name, style: :yaml, xdg: true, root_prefix: '')
42
+ @app_name = app_name.strip.downcase
43
+ raise ArgumentError, "reader app name may not be blank" if @app_name.blank?
44
+
45
+ msg = "reader app name may only contain letters, numbers, and underscores"
46
+ raise ArgumentError, msg unless app_name.match?(/\A[a-z][a-z0-9_]*\z/)
47
+
48
+ @root_prefix = root_prefix
49
+ @xdg = xdg
50
+
51
+ style = style.to_s.downcase.to_sym
52
+ @style =
53
+ case style
54
+ when :yaml
55
+ YAMLStyle.new
56
+ when :toml
57
+ TOMLStyle.new
58
+ when :ini
59
+ INIStyle.new
60
+ when :json
61
+ JSONStyle.new
62
+ else
63
+ msg = "config style must be one of #{VALID_CONFIG_STYLES.join(', ')}"
64
+ raise ArgumentError, msg
65
+ end
66
+ end
67
+
68
+ # Return a Hash of the config files for app_name directories. For
69
+ # applications that want to have more than one config file, a base name
70
+ # for the config file other than the app's name can be optionally
71
+ # provided.
72
+ #
73
+ # If you want a command-line to override config values, you can supply one
74
+ # as either a String or a Hash to the ~command_line:~ keyword parameter.
75
+ # If given a String, it must use the long-option form with equal signs for
76
+ # options to be given a value. If no equal sign and value are given, the
77
+ # option is assumed to be a boolean set to ~true~ unless the options
78
+ # starts with one of 'no', 'no-', or '!', in wich case the option is
79
+ # stripped of the negating prefix and the value is set to false. If given
80
+ # a Hash, it will be used unaltered.
81
+ #
82
+ # Finally, you can add a 'verbose: true' parameter to report the details
83
+ # of how the final Hash was formed on $stderr.
84
+ def read(alt_base = app_name, command_line: {}, verbose: false)
85
+ paths = config_paths(alt_base)
86
+ sys_configs = paths[:system]
87
+ usr_configs = paths[:user]
88
+ if verbose
89
+ if sys_configs.empty?
90
+ warn "No system config files found."
91
+ else
92
+ warn "System config files found: #{sys_configs.join('; ')}"
93
+ end
94
+ if usr_configs.empty?
95
+ warn "No user config files found."
96
+ else
97
+ warn "User config files found: #{usr_configs.join('; ')}"
98
+ end
99
+ end
100
+ result = style.merge_files(sys_configs, usr_configs, verbose: verbose)
101
+ result = merge_environment(result, verbose: verbose)
102
+ merge_command_line(result, command_line, verbose: verbose)
103
+ end
104
+
105
+ def merge_environment(start_hash, verbose: false)
106
+ return start_hash if ENV[env_name].blank?
107
+
108
+ env_hash = Hash.parse_opts(ENV[env_name])
109
+ if verbose
110
+ warn "Merging environment from #{env_name}:"
111
+ start_hash.report_merge(env_hash)
112
+ end
113
+ start_hash.merge(env_hash)
114
+ end
115
+
116
+ def merge_command_line(start_hash, command_line, verbose: false)
117
+ return start_hash unless command_line
118
+
119
+ return start_hash if command_line.empty?
120
+
121
+ cl_hash =
122
+ case command_line
123
+ when String
124
+ Hash.parse_opts(command_line)
125
+ when Hash
126
+ command_line
127
+ else
128
+ raise ArgumentError, "command_line must be a String or Hash"
129
+ end
130
+ if verbose
131
+ warn "Merging command-line:"
132
+ start_hash.report_merge(cl_hash)
133
+ end
134
+ start_hash.merge(cl_hash)
135
+ end
136
+
137
+ def env_name
138
+ "#{app_name.upcase}_OPTIONS"
139
+ end
140
+
141
+ def config_paths(base = app_name)
142
+ sys_configs = []
143
+ sys_env_name = "#{app_name.upcase}_SYS_CONFIG"
144
+ if ENV[sys_env_name]
145
+ sys_fname = File.join(root_prefix, File.expand_path(ENV[sys_env_name]))
146
+ sys_configs << sys_fname if File.readable?(sys_fname)
147
+ else
148
+ sys_configs +=
149
+ if xdg
150
+ find_xdg_sys_config_files(base)
151
+ else
152
+ find_classic_sys_config_files(base)
153
+ end
154
+ end
155
+
156
+ usr_configs = []
157
+ usr_env_name = "#{app_name.upcase}_CONFIG"
158
+ if ENV[usr_env_name]
159
+ usr_fname = File.join(root_prefix, File.expand_path(ENV[usr_env_name]))
160
+ usr_configs << usr_fname if File.readable?(usr_fname)
161
+ else
162
+ usr_configs <<
163
+ if xdg
164
+ find_xdg_user_config_file(base)
165
+ else
166
+ find_classic_user_config_file(base)
167
+ end
168
+ end
169
+ { system: sys_configs.compact, user: usr_configs.compact }
170
+ end
171
+
172
+ ########################################################################
173
+ # XDG config files
174
+ ########################################################################
175
+
176
+ # From the XDG standard:
177
+ # Your application should store and load data and configuration files to/from
178
+ # the directories pointed by the following environment variables:
179
+ #
180
+ # $XDG_CONFIG_HOME (default: "$HOME/.config"): user-specific configuration files.
181
+ # $XDG_CONFIG_DIRS (default: "/etc/xdg"): precedence-ordered set of system configuration directories.
182
+
183
+ # Return the absolute path names of all XDG system config files for
184
+ # app_name with the basename variants of base. Return the lowest priority
185
+ # files first, highest last. Prefix the search locations with dir_prefix
186
+ # if given.
187
+ def find_xdg_sys_config_files(base = app_name)
188
+ configs = []
189
+ xdg_search_dirs = ENV['XDG_CONFIG_DIRS']&.split(':')&.reverse || ['/etc/xdg']
190
+ xdg_search_dirs.each do |dir|
191
+ dir = File.expand_path(File.join(dir, app_name))
192
+ dir = File.join(root_prefix, dir) unless root_prefix.nil? || root_prefix.strip.empty?
193
+ base_candidates = style.dir_constrained_base_names(base)
194
+ config_fname = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
195
+ configs << File.join(dir, config_fname) if config_fname
196
+ end
197
+ configs
198
+ end
199
+
200
+ # Return the absolute path name of any XDG user config files for app_name
201
+ # with the basename variants of base. The XDG_CONFIG_HOME environment
202
+ # variable for the user configs is intended to be the name of a single xdg
203
+ # config directory, not a list of colon-separated directories as for the
204
+ # system config. Return the name of a config file for this app in
205
+ # XDG_CONFIG_HOME (or ~/.config by default). Prefix the search location
206
+ # with dir_prefix if given.
207
+ def find_xdg_user_config_file(base = app_name)
208
+ xdg_search_dir = ENV['XDG_CONFIG_HOME'] || ['~/.config']
209
+ dir = File.expand_path(File.join(xdg_search_dir, app_name))
210
+ dir = File.join(root_prefix, dir) unless root_prefix.strip.empty?
211
+ return unless Dir.exist?(dir)
212
+
213
+ base_candidates = style.dir_constrained_base_names(base)
214
+ config_fname = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
215
+ if config_fname
216
+ File.join(dir, config_fname)
217
+ end
218
+ end
219
+
220
+ ########################################################################
221
+ # Classic config files
222
+ ########################################################################
223
+
224
+ # Return the absolute path names of all "classic" system config files for
225
+ # app_name with the basename variants of base. Return the lowest priority
226
+ # files first, highest last. Prefix the search locations with dir_prefix
227
+ # if given.
228
+ def find_classic_sys_config_files(base = app_name)
229
+ configs = []
230
+ env_config = ENV["#{app_name.upcase}_SYS_CONFIG"]
231
+ if env_config && File.readable?((config = File.join(root_prefix, File.expand_path(env_config))))
232
+ configs = [config]
233
+ elsif File.readable?(config = File.join(root_prefix, "/etc/#{base}"))
234
+ configs = [config]
235
+ elsif File.readable?(config = File.join(root_prefix, "/etc/#{base}rc"))
236
+ configs = [config]
237
+ else
238
+ dir = File.join(root_prefix, "/etc/#{app_name}")
239
+ if Dir.exist?(dir)
240
+ base_candidates = style.classic_base_names(base)
241
+ config = base_candidates.find { |b| File.readable?(File.join(dir, b)) }
242
+ configs = [File.join(dir, config)] if config
243
+ end
244
+ end
245
+ configs
246
+ end
247
+
248
+ # Return the absolute path names of all "classic" system config files for
249
+ # app_name with the basename variants of base. Return the lowest priority
250
+ # files first, highest last. Prefix the search locations with dir_prefix if
251
+ # given.
252
+ def find_classic_user_config_file(base = app_name)
253
+ env_config = ENV["#{app_name.upcase}_CONFIG"]
254
+ if env_config && File.readable?((config = File.join(root_prefix, File.expand_path(env_config))))
255
+ config
256
+ else
257
+ config_dir = File.join(root_prefix, File.expand_path("~/"))
258
+ base_candidates = style.dotted_base_names(base)
259
+ base_fname = base_candidates.find do |b|
260
+ File.file?(File.join(config_dir, b)) && File.readable?(File.join(config_dir, b))
261
+ end
262
+ if base_fname
263
+ File.join(config_dir, base_fname)
264
+ elsif Dir.exist?(config_dir = File.join(root_prefix, File.expand_path("~/.#{app_name}")))
265
+ base_candidates = style.dir_constrained_base_names(base)
266
+ base_fname = base_candidates.find { |b| File.readable?(File.join(config_dir, b)) }
267
+ File.join(config_dir, base_fname) if base_fname
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,73 @@
1
+ module FatConfig
2
+ # This class acts as a super class for specific styles of config files. The
3
+ # subclass must provide a load_file method that takes a file name, reads it
4
+ # according to the rules of the Config style, and returns a Hash of the
5
+ # config values. The extent to which values are de-serialized from string
6
+ # is up to the subclass.
7
+ class Style
8
+ # Read the file with the given name and return a Hash represented by the
9
+ # config file.
10
+ def load_file(file_name)
11
+ raise NotImplementedError, "Style#load_file must be defined in a subclass of Style"
12
+ end
13
+
14
+ # The possible file extensions for files of this Style. Here, we give the
15
+ # generic config extensions, but the subclass should supplement it.
16
+ def possible_extensions
17
+ ['cfg', 'config']
18
+ end
19
+
20
+ # Read all the given system and user-level config files in order from
21
+ # lower priority to higher priority, merging each along the way to build
22
+ # the final Hash. If requested, report the details to $stderr.
23
+ def merge_files(sys_files = [], usr_files = [], verbose: false)
24
+ hash = {}
25
+ files = (sys_files + usr_files).compact
26
+ files.each do |f|
27
+ next unless File.readable?(f)
28
+
29
+ file_hash = load_file(f)
30
+ next unless file_hash
31
+
32
+ if file_hash.is_a?(Hash)
33
+ file_hash = file_hash.methodize
34
+ else
35
+ raise "Error loading file #{f}:\n#{File.read(f)[0..500]}"
36
+ end
37
+ if verbose
38
+ warn "Merging system config from file '#{f}':" if sys_files.include?(f)
39
+ warn "Merging user config from file '#{f}':" if usr_files.include?(f)
40
+ hash.report_merge(file_hash)
41
+ end
42
+ hash.deep_merge!(file_hash)
43
+ end
44
+ hash
45
+ end
46
+
47
+ # Return a list of possible configuration file basenames where the
48
+ # directory path DOES NOT already include the app_name so that the
49
+ # basename itself must be distingashable as belonging to the app.
50
+ def constrained_base_names(base)
51
+ [base] + possible_extensions.map { |ext| "#{base}.#{ext}" }
52
+ end
53
+
54
+ # Return a list of possible configuration file basenames where the
55
+ # directory path DOES already includes the app_name so that it need not be
56
+ # included in the basename itself.
57
+ def dir_constrained_base_names(base)
58
+ constrained_base_names(base) + ['config'] + possible_extensions.map { |ext| "config.#{ext}" }
59
+ end
60
+
61
+ # Return a list of possible configuration file basenames as might be
62
+ # placed in the user's home directory as a hidden file, but which need to
63
+ # contain a component of the app_name to distinguish it.
64
+ def dotted_base_names(base)
65
+ [".#{base}", ".#{base}rc"] + possible_extensions.map { |ext| ".#{base}.#{ext}" }
66
+ end
67
+ end
68
+ end
69
+
70
+ require_relative 'styles/yaml_style'
71
+ require_relative 'styles/toml_style'
72
+ require_relative 'styles/ini_style'
73
+ require_relative 'styles/json_style'
@@ -0,0 +1,23 @@
1
+ module FatConfig
2
+ class INIStyle < Style
3
+ def load_string(str)
4
+ # Since INIFile does not have a method for parsing strings, we have to
5
+ # create a file with the string as content.
6
+ tmp_path = File.join("/tmp", "fat_config/ini#{$PID}")
7
+ File.write(tmp_path, str)
8
+ load_file(tmp_path)
9
+ rescue IniFile::Error => ex
10
+ raise FatConfig::ParseError, ex.to_s
11
+ end
12
+
13
+ def load_file(file_name)
14
+ IniFile.load(file_name).to_h.methodize
15
+ rescue IniFile::Error => ex
16
+ raise FatConfig::ParseError, ex.to_s
17
+ end
18
+
19
+ def possible_extensions
20
+ super + ['ini']
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module FatConfig
2
+ class JSONStyle < Style
3
+ def load_string(str)
4
+ JSON.parse(str, symbolize_name: true).methodize
5
+ rescue JSON::ParserError => ex
6
+ raise FatConfig::ParseError, ex.to_s
7
+ end
8
+
9
+ def load_file(file_name)
10
+ load_string(File.read(file_name))
11
+ end
12
+
13
+ def possible_extensions
14
+ super + ['json']
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module FatConfig
2
+ class TOMLStyle < Style
3
+ def load_string(str)
4
+ Tomlib.load(str)&.methodize
5
+ rescue Tomlib::ParseError => ex
6
+ raise FatConfig::ParseError, ex.to_s
7
+ end
8
+
9
+ def load_file(file_name)
10
+ load_string(File.read(file_name))
11
+ end
12
+
13
+ def possible_extensions
14
+ super + ['toml']
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ require 'date'
2
+
3
+ module FatConfig
4
+ # NOTE: from the Psych documentation, some types are 'deserialized',
5
+ # meaning they are converted to Ruby objects. For example, a value of
6
+ # '10' for a property will be converted to the integer 10.
7
+ #
8
+ # Safely load the yaml string in yaml. By default, only the
9
+ # following classes are allowed to be deserialized:
10
+ #
11
+ # - TrueClass
12
+ # - FalseClass
13
+ # - NilClass
14
+ # - Integer
15
+ # - Float
16
+ # - String
17
+ # - Array
18
+ # - Hash
19
+ #
20
+ # Recursive data structures are not allowed by default. Arbitrary classes
21
+ # can be allowed by adding those classes to the permitted_classes
22
+ # keyword argument. They are additive. For example, to allow Date
23
+ # deserialization:
24
+ #
25
+ # Config.read adds Date, etc., to permitted classes, but provides for no others.
26
+ class YAMLStyle < Style
27
+ def load_string(str)
28
+ Psych.safe_load(
29
+ str,
30
+ symbolize_names: true,
31
+ permitted_classes: [Date, DateTime, Time],
32
+ )&.methodize || {}
33
+ rescue Psych::SyntaxError => ex
34
+ raise FatConfig::ParseError, ex.to_s
35
+ end
36
+
37
+ def load_file(file_name)
38
+ Psych.safe_load_file(
39
+ file_name,
40
+ symbolize_names: true,
41
+ permitted_classes: [Date, DateTime, Time],
42
+ )&.methodize || {}
43
+ rescue Psych::SyntaxError => ex
44
+ raise FatConfig::ParseError, ex.to_s
45
+ end
46
+
47
+ def possible_extensions
48
+ super + ['yml', 'yaml']
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FatConfig
4
+ VERSION = "0.4.1"
5
+ end
data/lib/fat_config.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash'
4
+ require 'fileutils'
5
+ require 'psych'
6
+ require 'tomlib'
7
+ require 'inifile'
8
+ require 'json'
9
+
10
+ require_relative "fat_config/version"
11
+ require_relative "fat_config/errors"
12
+ require_relative "fat_config/core_ext/hash_ext"
13
+ require_relative "fat_config/reader"
14
+ require_relative "fat_config/style"
15
+
16
+ module FatConfig
17
+ class Error < StandardError; end
18
+ # Your code goes here...
19
+ end
@@ -0,0 +1,178 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-performance
4
+ - rubocop-rake
5
+
6
+ inherit_gem:
7
+ rubocop-shopify: rubocop.yml
8
+
9
+ AllCops:
10
+ NewCops: enable
11
+ # TargetRubyVersion: 3.0
12
+
13
+ Style/DateTime:
14
+ Enabled: false
15
+
16
+ Style/StringLiteralsInInterpolation:
17
+ Enabled: true
18
+ EnforcedStyle: single_quotes
19
+
20
+ Style/MethodCallWithArgsParentheses:
21
+ Enabled: false
22
+
23
+ Style/StringLiterals:
24
+ Enabled: false
25
+
26
+ Style/WordArray:
27
+ Enabled: false
28
+
29
+ Style/SymbolArray:
30
+ Enabled: false
31
+
32
+ Style/TrailingCommaInHashLiteral:
33
+ Enabled: false
34
+
35
+ Style/TrailingCommaInArrayLiteral:
36
+ Enabled: false
37
+
38
+ Style/HashSyntax:
39
+ Enabled: false
40
+
41
+ Style/ClassMethodsDefinitions:
42
+ Enabled: true
43
+ EnforcedStyle: def_self
44
+
45
+ Layout/LineLength:
46
+ Enabled: true
47
+ Max: 120
48
+ # To make it possible to copy or click on URIs in the code, we allow lines
49
+ # containing a URI to be longer than Max.
50
+ AllowHeredoc: true
51
+ AllowURI: true
52
+ URISchemes:
53
+ - http
54
+ - https
55
+
56
+ Layout/ArgumentAlignment:
57
+ Enabled: true
58
+ EnforcedStyle: with_first_argument
59
+
60
+ Naming/InclusiveLanguage:
61
+ Enabled: false
62
+
63
+ Metrics/AbcSize:
64
+ # The ABC size is a calculated magnitude, so this number can be a Fixnum or
65
+ # a Float.
66
+ Enabled: false
67
+ Max: 50
68
+
69
+ Metrics/BlockNesting:
70
+ Enabled: false
71
+ Max: 3
72
+
73
+ Metrics/BlockLength:
74
+ Enabled: false
75
+ Max: 25
76
+
77
+ Metrics/ClassLength:
78
+ Enabled: false
79
+ CountComments: false # count full line comments?
80
+ Max: 100
81
+
82
+ Metrics/ModuleLength:
83
+ Enabled: false
84
+ CountComments: false # count full line comments?
85
+ Max: 100
86
+
87
+ Metrics/MethodLength:
88
+ Enabled: false
89
+ CountComments: false # count full line comments?
90
+ Max: 10
91
+
92
+ # Avoid complex methods.
93
+ Metrics/CyclomaticComplexity:
94
+ Enabled: false
95
+ Max: 20
96
+
97
+ Metrics/ParameterLists:
98
+ Max: 5
99
+ CountKeywordArgs: false
100
+
101
+ Metrics/PerceivedComplexity:
102
+ Enabled: false
103
+ Max: 8
104
+
105
+ Layout/MultilineOperationIndentation:
106
+ EnforcedStyle: aligned
107
+
108
+ Layout/MultilineMethodCallIndentation:
109
+ EnforcedStyle: indented_relative_to_receiver
110
+ SupportedStyles:
111
+ - aligned
112
+ - indented
113
+ - indented_relative_to_receiver
114
+ # By default, the indentation width from Style/IndentationWidth is used
115
+ # But it can be overridden by setting this parameter
116
+ IndentationWidth: ~
117
+
118
+ # Though the style guides recommend against them, I like perl back references.
119
+ # They are much more concise than the recommended: $2 vs. Regexp.last_match(2).
120
+ # Two characters versus 18!
121
+ # Cop supports --auto-correct.
122
+ Style/PerlBackrefs:
123
+ Enabled: false
124
+
125
+ # Cop supports --auto-correct.
126
+ # Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods.
127
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining
128
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
129
+ # FunctionalMethods: let, let!, subject, watch
130
+ # IgnoredMethods: lambda, proc, it
131
+ Style/BlockDelimiters:
132
+ EnforcedStyle: braces_for_chaining
133
+ ProceduralMethods: expect
134
+
135
+ # Cop supports --auto-correct.
136
+ # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
137
+ Layout/ExtraSpacing:
138
+ AllowForAlignment: true
139
+
140
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
141
+ # SupportedStyles: format, sprintf, percent
142
+ Style/FormatString:
143
+ Enabled: false
144
+
145
+ # Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
146
+ # NamePrefix: is_, has_, have_
147
+ # NamePrefixBlacklist: is_, has_, have_
148
+ # NameWhitelist: is_a?
149
+ Naming/PredicateName:
150
+ AllowedMethods: has_overlaps_within?
151
+ Exclude:
152
+ - 'spec/**/*'
153
+
154
+ # Cop supports --auto-correct.
155
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
156
+ # SupportedStyles: always, never
157
+ Style/FrozenStringLiteralComment:
158
+ Enabled: false
159
+ EnforcedStyle: always
160
+
161
+ # I like using !! to convert a value to boolean.
162
+ Style/DoubleNegation:
163
+ Enabled: false
164
+
165
+ RSpec/MultipleExpectations:
166
+ Enabled: false
167
+
168
+ RSpec/ExampleLength:
169
+ Enabled: false
170
+
171
+ RSpec/DescribedClass:
172
+ Enabled: false
173
+
174
+ RSpec/MultipleMemoizedHelpers:
175
+ Max: 10
176
+
177
+ RSpec/NestedGroups:
178
+ Max: 5
@@ -0,0 +1,4 @@
1
+ module FatConfig
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fat_config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Daniel E. Doherty
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2024-12-31 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: fat_core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: inifile
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tomlib
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: |2+
69
+
70
+ This library provides a reader for configuration files, looking for them in places
71
+ designated by (1) a user-set environment variable, (2) in the standard XDG
72
+ locations (e.g., /etc/xdg/app.yml), or (3) in the classical UNIX locations
73
+ (e.g. /etc/app/config.yml or ~/.apprc). Config files can be written in one of
74
+ YAML, TOML, INI-style, or JSON. It enforces precedence of user-configs over
75
+ system-level configs, and enviroment or command-line configs over the file-based
76
+ configs.
77
+
78
+ email:
79
+ - ded@ddoherty.net
80
+ executables: []
81
+ extensions: []
82
+ extra_rdoc_files: []
83
+ files:
84
+ - ".rspec"
85
+ - ".rubocop.yml"
86
+ - LICENSE.txt
87
+ - README.org
88
+ - Rakefile
89
+ - TODO.org
90
+ - lib/fat_config.rb
91
+ - lib/fat_config/core_ext/hash_ext.rb
92
+ - lib/fat_config/errors.rb
93
+ - lib/fat_config/reader.rb
94
+ - lib/fat_config/style.rb
95
+ - lib/fat_config/styles/ini_style.rb
96
+ - lib/fat_config/styles/json_style.rb
97
+ - lib/fat_config/styles/toml_style.rb
98
+ - lib/fat_config/styles/yaml_style.rb
99
+ - lib/fat_config/version.rb
100
+ - rubocop-global.yml
101
+ - sig/fat_config.rbs
102
+ homepage: https://git.ddoherty.net/fat_config
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ allowed_push_host: https://rubygems.org
107
+ homepage_uri: https://git.ddoherty.net/fat_config
108
+ source_code_uri: https://git.ddoherty.net/fat_config
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 3.0.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.6.2
124
+ specification_version: 4
125
+ summary: Library to read config from standard XDG or classic locations.
126
+ test_files: []
127
+ ...