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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/LICENSE.txt +21 -0
- data/README.org +315 -0
- data/Rakefile +12 -0
- data/TODO.org +11 -0
- data/lib/fat_config/core_ext/hash_ext.rb +91 -0
- data/lib/fat_config/errors.rb +3 -0
- data/lib/fat_config/reader.rb +272 -0
- data/lib/fat_config/style.rb +73 -0
- data/lib/fat_config/styles/ini_style.rb +23 -0
- data/lib/fat_config/styles/json_style.rb +17 -0
- data/lib/fat_config/styles/toml_style.rb +17 -0
- data/lib/fat_config/styles/yaml_style.rb +51 -0
- data/lib/fat_config/version.rb +5 -0
- data/lib/fat_config.rb +19 -0
- data/rubocop-global.yml +178 -0
- data/sig/fat_config.rbs +4 -0
- metadata +127 -0
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
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
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,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
|
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
|
data/rubocop-global.yml
ADDED
@@ -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
|
data/sig/fat_config.rbs
ADDED
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
|
+
...
|