unfig 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/linters.yml +37 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.gitignore +7 -0
- data/.mdl_rules.rb +2 -0
- data/.mdlrc +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.standard.yml +3 -0
- data/Gemfile +3 -0
- data/README.md +100 -0
- data/lib/unfig/argv_loader.rb +94 -0
- data/lib/unfig/env_loader.rb +25 -0
- data/lib/unfig/env_reader.rb +72 -0
- data/lib/unfig/file_loader.rb +76 -0
- data/lib/unfig/loader.rb +68 -0
- data/lib/unfig/param_config.rb +70 -0
- data/lib/unfig/param_validator.rb +157 -0
- data/lib/unfig/params_config.rb +63 -0
- data/lib/unfig/version.rb +3 -0
- data/lib/unfig.rb +29 -0
- data/unfig.gemspec +45 -0
- metadata +196 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3ff007a57b57a0e32f859b15cbdfb268353a34448499426561c04b4c2f0ca085
|
|
4
|
+
data.tar.gz: bc50b16f98ab7f74c8cf69fbda12f9af7b975107edc678fb41e383999dbf6b71
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 12c22f5cfc6d564b46d84482bd0272680b41fa1cd461386009afb51833938f98d5305ead5195cd72d9f1f381843d489ef55834270f46d6a74bf6fd6dc29abdbe
|
|
7
|
+
data.tar.gz: f2230b52d0a0c6f4b08e98f03ee9733a25532b16c705704d38b4618fd95d6d794d1a3a812a7065570435f48e41d9e67c403a2430254136361c931a52a4af9ad6
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Linters
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
Linting:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
|
|
11
|
+
- name: Set up ruby
|
|
12
|
+
uses: ruby/setup-ruby@v1
|
|
13
|
+
with:
|
|
14
|
+
ruby-version: 3.3
|
|
15
|
+
|
|
16
|
+
- name: Cache gems
|
|
17
|
+
uses: actions/cache@v3
|
|
18
|
+
with:
|
|
19
|
+
path: vendor/bundle
|
|
20
|
+
key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.log') }}
|
|
21
|
+
restore-keys:
|
|
22
|
+
${{ runner.os }}-linters-
|
|
23
|
+
|
|
24
|
+
- name: Install gems
|
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
|
26
|
+
|
|
27
|
+
- name: Run StandardRB
|
|
28
|
+
run: bundle exec standardrb
|
|
29
|
+
|
|
30
|
+
- name: Run rubocop (complexity checks)
|
|
31
|
+
run: bundle exec rubocop --parallel
|
|
32
|
+
|
|
33
|
+
- name: Run markdownlint
|
|
34
|
+
run: bundle exec mdl .
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: RSpec
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
RSpec:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
fail-fast: false
|
|
10
|
+
matrix:
|
|
11
|
+
ruby-version: ['3.2', '3.3', '3.4', 'head']
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
20
|
+
|
|
21
|
+
- name: Cache gems
|
|
22
|
+
uses: actions/cache@v3
|
|
23
|
+
with:
|
|
24
|
+
path: vendor/bundle
|
|
25
|
+
key: ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
|
|
26
|
+
restore-keys:
|
|
27
|
+
${{ runner.os }}-rspec-${{ matrix.ruby-version }}-
|
|
28
|
+
|
|
29
|
+
- name: Install gems
|
|
30
|
+
run: bundle install --jobs 4 --retry 3
|
|
31
|
+
|
|
32
|
+
- name: Run RSpec
|
|
33
|
+
run: SIMPLECOV=true bundle exec rspec
|
data/.gitignore
ADDED
data/.mdl_rules.rb
ADDED
data/.mdlrc
ADDED
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
AllCops:
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
DisabledByDefault: true
|
|
5
|
+
|
|
6
|
+
Metrics/AbcSize:
|
|
7
|
+
Max: 15
|
|
8
|
+
Metrics/CyclomaticComplexity:
|
|
9
|
+
Max: 8
|
|
10
|
+
Metrics/PerceivedComplexity:
|
|
11
|
+
Max: 7
|
|
12
|
+
|
|
13
|
+
Metrics/ClassLength:
|
|
14
|
+
CountComments: false
|
|
15
|
+
Max: 150
|
|
16
|
+
Metrics/MethodLength:
|
|
17
|
+
CountComments: false
|
|
18
|
+
Max: 15
|
|
19
|
+
Metrics/ParameterLists:
|
|
20
|
+
Max: 5
|
|
21
|
+
CountKeywordArgs: true
|
data/.standard.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Unfig
|
|
2
|
+
|
|
3
|
+
We've written code that merges/cascades some combination of defaults, config-files,
|
|
4
|
+
environment-variables, and cli-supplied parameters *too many times*. In one of my
|
|
5
|
+
gems, _half of the code_ is concerned with managing those various ways to supply
|
|
6
|
+
its 20+ control parameters. That's ridiculous.
|
|
7
|
+
|
|
8
|
+
The intent of `unfig` is to consolidate all of that capability, so that we
|
|
9
|
+
just need to specify what configuration exists (and optionally how it can be
|
|
10
|
+
supplied) in a straightforward way, and stop worrying about it.
|
|
11
|
+
|
|
12
|
+
## Usage Example (currently tentative)
|
|
13
|
+
|
|
14
|
+
Let's invoke Unfig like this, as an example:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "unfig"
|
|
18
|
+
|
|
19
|
+
@options = Unfig.load_options(
|
|
20
|
+
config: File.expand_path("~/.mygem.yml"),
|
|
21
|
+
values: {
|
|
22
|
+
verbose: {
|
|
23
|
+
description: "Expose much more information in the logs",
|
|
24
|
+
type: "boolean",
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
parallel: {
|
|
28
|
+
description: "Run in parallel instead of serially",
|
|
29
|
+
type: "boolean",
|
|
30
|
+
default: false,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Now I'll have an `@options` object (a Hash, since we didn't specify), and the
|
|
37
|
+
user can supply either of those options on the command-line, in the environment,
|
|
38
|
+
or in a config-file in their home directory.
|
|
39
|
+
|
|
40
|
+
It's more configurable than that of course. At the top-level, it accepts these
|
|
41
|
+
options:
|
|
42
|
+
|
|
43
|
+
* `argv` - Usually this is `ARGV` (the default), but if you want to interact
|
|
44
|
+
with the separately, you could supply `argv: ARGV.dup`, or just an array
|
|
45
|
+
of arguments.
|
|
46
|
+
* `env` - Usually this is `ENV` (the default), but if you want, you can just
|
|
47
|
+
pass an arbitrary Hash(String => String) here instead.
|
|
48
|
+
* `config` - where to search for the (yaml) config file that might specify some
|
|
49
|
+
of these parameters. Optional - if not supplied, no config-file will be used.
|
|
50
|
+
* `banner` - what to say on the command-line when help is invoked (usually a
|
|
51
|
+
usage example). Optional - if not supplied, no banner is present.
|
|
52
|
+
* `format` - what sort of object comes out of the `load` call; one of
|
|
53
|
+
`[:hash, :struct, :openstruct]`. If not supplied, defaults to `:hash`.
|
|
54
|
+
* `params` - the parameter configurations; keys are parameter names (symbol or
|
|
55
|
+
string), and values are parameter configs.
|
|
56
|
+
|
|
57
|
+
And for each parameter, we have these options:
|
|
58
|
+
|
|
59
|
+
* `name` - this is the key in the params hash. It can be a String or Symbol,
|
|
60
|
+
it must start with a letter, and it may contain only letters, numbers, and
|
|
61
|
+
underscores. No more than 64 characters long.
|
|
62
|
+
* `description` - this will be used as the description of the parameter on the
|
|
63
|
+
cli - it is required, must be a non-blank string, and may contain no newlines.
|
|
64
|
+
* `type` - this is the type to cast the supplied values into - it is required,
|
|
65
|
+
and must be one of "boolean", "string", "integer", or "float" (a String).
|
|
66
|
+
* `multi` - this specifies whether the parameter can be "multi-valued" (default
|
|
67
|
+
is false). If true, supplying the flag more than once, or supplying an array
|
|
68
|
+
of values in the config file, or (more awkwardly) setting multiple environment
|
|
69
|
+
variables with numeric suffixes (`FOO_0` through `FOO_9` for an option `foo`)
|
|
70
|
+
will allow you to supply multiple values, and the parameter will return an
|
|
71
|
+
Array (whether you actually do so or not).
|
|
72
|
+
* `enabled` - which configuration methods are supported for the parameter. By
|
|
73
|
+
default all of them will be, but you can for example exclude `verbose` from
|
|
74
|
+
being supplied via the config file if you want to, or only support the _long_
|
|
75
|
+
flag (`--verbose` works but not `-v`).
|
|
76
|
+
* `default` - what value should this parameter have if they don't supply one?
|
|
77
|
+
If you supplied `multi: true`, this is required to be an array of defaults;
|
|
78
|
+
in either case, the _type_ of the default will be validated against the
|
|
79
|
+
supplied `type`.
|
|
80
|
+
* `long`/`short` - these can override the default long/short cli flags. This is
|
|
81
|
+
particularly important for `short`, since two parameters that start with the
|
|
82
|
+
same letter will automatically collide unless it's supplied for one of them.
|
|
83
|
+
The default values are (a) the parameter-name, but with underscores mapped to
|
|
84
|
+
dashes and (b) the first letter of the parameter name.
|
|
85
|
+
* `env` - the name of the environment variable to consult. By default, it's the
|
|
86
|
+
parameter name up-cased, but like.. if your parameter is `user`, you may need
|
|
87
|
+
to look at `MYGEM_USER` instead for example (unless you _want_ the unix USER).
|
|
88
|
+
|
|
89
|
+
The configurations will be loaded from all four sources, in this priority order:
|
|
90
|
+
|
|
91
|
+
* cli flags - if the user supplies these, they are likely intending to override
|
|
92
|
+
default or configured behaviors.
|
|
93
|
+
* environment variables - these are a typical way to configure a machine with
|
|
94
|
+
its intended behavior, though specifying a parameter in a *personal* config
|
|
95
|
+
file _and_ an environment variable is infrequent, sometimes config files are
|
|
96
|
+
committed to a repository, and an individual may want to consistently override
|
|
97
|
+
values supplied in that way.
|
|
98
|
+
* config file - this could be a personal config (in your home directory) or a
|
|
99
|
+
provided config (committed to your rails app's config/ directory, for example).
|
|
100
|
+
* default value - supplied when invoking Unfig in the script.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class ArgvLoader
|
|
3
|
+
def initialize(params:, argv:)
|
|
4
|
+
@params = params
|
|
5
|
+
@argv = argv
|
|
6
|
+
@options = {}
|
|
7
|
+
@stop_early = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def read
|
|
11
|
+
return @_read if defined?(@_read)
|
|
12
|
+
option_parser.parse!(argv)
|
|
13
|
+
@_read = @stop_early ? nil : @options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :params, :argv
|
|
19
|
+
|
|
20
|
+
def short_enabled?(p) = p.enabled.include?("short")
|
|
21
|
+
|
|
22
|
+
def long_enabled?(p) = p.enabled.include?("long")
|
|
23
|
+
|
|
24
|
+
def enabled?(p) = short_enabled?(p) || long_enabled?(p)
|
|
25
|
+
|
|
26
|
+
def option_parser
|
|
27
|
+
@_option_parser ||= OptionParser.new do |opts|
|
|
28
|
+
opts.banner = params.banner if params.banner
|
|
29
|
+
add_help_option(opts)
|
|
30
|
+
|
|
31
|
+
params.params.each do |p|
|
|
32
|
+
next unless enabled?(p)
|
|
33
|
+
|
|
34
|
+
add_option_for(opts, p)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def short_arg(p)
|
|
40
|
+
if p.type == "boolean"
|
|
41
|
+
"-#{p.short}"
|
|
42
|
+
else
|
|
43
|
+
"-#{p.short}#{p.name.upcase}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def long_arg(p)
|
|
48
|
+
if p.type == "boolean"
|
|
49
|
+
"--[no-]#{p.long}"
|
|
50
|
+
else
|
|
51
|
+
"--#{p.long}=#{p.name.upcase}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def type_arg(p)
|
|
56
|
+
case p.type
|
|
57
|
+
when "string" then String
|
|
58
|
+
when "boolean" then TrueClass
|
|
59
|
+
when "integer" then Integer
|
|
60
|
+
when "float" then Numeric
|
|
61
|
+
else
|
|
62
|
+
raise Invalid, "ArgvLoader does not know how to handle type '#{p.type}'"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def option_args(p)
|
|
67
|
+
args = []
|
|
68
|
+
args << short_arg(p) if short_enabled?(p)
|
|
69
|
+
args << long_arg(p) if long_enabled?(p)
|
|
70
|
+
args << type_arg(p)
|
|
71
|
+
args << p.description
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_option_for(opts, p)
|
|
75
|
+
opts.on(*option_args(p)) do |value|
|
|
76
|
+
if p.multi?
|
|
77
|
+
@options[p.name] ||= []
|
|
78
|
+
@options[p.name] << value
|
|
79
|
+
elsif @options.key?(p.name)
|
|
80
|
+
raise FlagError, "Cannot supply #{p.name} more than once"
|
|
81
|
+
else
|
|
82
|
+
@options[p.name] = value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_help_option(opts)
|
|
88
|
+
opts.on_tail("-h", "--help", "Print this help information") do
|
|
89
|
+
warn(opts)
|
|
90
|
+
@stop_early = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class EnvLoader
|
|
3
|
+
def initialize(params:, env:)
|
|
4
|
+
@params = params
|
|
5
|
+
@env = env
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def read
|
|
9
|
+
return @_read if defined?(@_read)
|
|
10
|
+
|
|
11
|
+
@_read = {}
|
|
12
|
+
params.params.each do |p|
|
|
13
|
+
next unless p.enabled.include?("env")
|
|
14
|
+
|
|
15
|
+
reader = EnvReader.new(param: p, env: env)
|
|
16
|
+
@_read[p.name] = reader.value if reader.supplied?
|
|
17
|
+
end
|
|
18
|
+
@_read
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :params, :env
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class EnvReader
|
|
3
|
+
TRUTHY_STRINGS = %w[true yes on enable allow t y 1 ok okay].to_set.freeze
|
|
4
|
+
FALSEY_STRINGS = %w[false no off disabled disable deny f n 0 nope].to_set.freeze
|
|
5
|
+
|
|
6
|
+
def initialize(param:, env:)
|
|
7
|
+
@param = param
|
|
8
|
+
@env = env
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def supplied? = multi? ? multi_keys_supplied? : key_supplied?
|
|
12
|
+
|
|
13
|
+
def value
|
|
14
|
+
if multi?
|
|
15
|
+
supplied? ? multi_values : []
|
|
16
|
+
else
|
|
17
|
+
supplied? ? single_value : nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :param, :env
|
|
24
|
+
|
|
25
|
+
def multi? = param.multi?
|
|
26
|
+
|
|
27
|
+
def multi_keys = @_multi_keys ||= [param.env] + (0..9).map { |n| "#{param.env}_#{n}" }
|
|
28
|
+
|
|
29
|
+
def supplied_multi_keys = multi_keys.select { |k| env.key?(k) }
|
|
30
|
+
|
|
31
|
+
def multi_keys_supplied?
|
|
32
|
+
return @_multi_keys_supplied if defined?(@_multi_keys_supplied)
|
|
33
|
+
|
|
34
|
+
@_multi_keys_supplied = multi_keys.any? { |key| env.key?(key) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def multi_values = supplied_multi_keys.map { |k| cast_env(k, env.fetch(k)) }
|
|
38
|
+
|
|
39
|
+
def single_value = cast_env(param.env, env.fetch(param.env))
|
|
40
|
+
|
|
41
|
+
def key_supplied? = env.key?(param.env)
|
|
42
|
+
|
|
43
|
+
def cast_env(name, uncast)
|
|
44
|
+
case param.type
|
|
45
|
+
when "string" then uncast
|
|
46
|
+
when "boolean" then cast_to_boolean(name, uncast)
|
|
47
|
+
when "integer" then cast_to_integer(name, uncast)
|
|
48
|
+
when "float" then cast_to_float(name, uncast)
|
|
49
|
+
else
|
|
50
|
+
raise Invalid, "Unfig::EnvLoader does not know how to handle parameter type '#{param.type}'"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cast_to_boolean(name, s)
|
|
55
|
+
return true if TRUTHY_STRINGS.include?(s.strip.downcase)
|
|
56
|
+
return false if FALSEY_STRINGS.include?(s.strip.downcase)
|
|
57
|
+
raise InvalidBooleanText, "ENV['#{name}'] had unexpected content for a boolean: '#{s}'"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cast_to_integer(name, s)
|
|
61
|
+
return s.to_i if /\A-?\d+\z/.match?(s.strip)
|
|
62
|
+
|
|
63
|
+
raise InvalidIntegerText, "ENV['#{name}'] had unexpected content for an integer: '#{s}'"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cast_to_float(name, s)
|
|
67
|
+
return s.to_f if /\A-?\d+(\.\d+)?\z/.match?(s.strip)
|
|
68
|
+
|
|
69
|
+
raise InvalidFloatingPointText, "ENV['#{name}'] had unexpected content for a float: '#{s}'"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class FileLoader
|
|
3
|
+
def initialize(params:, path:)
|
|
4
|
+
@params = params
|
|
5
|
+
@path = path
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def read
|
|
9
|
+
return @_read if defined?(@_read)
|
|
10
|
+
|
|
11
|
+
@_read = {}
|
|
12
|
+
params.params.each do |p|
|
|
13
|
+
next unless p.enabled.include?("file")
|
|
14
|
+
@_read[p.name] = read_for(p) if data.key?(p.name)
|
|
15
|
+
end
|
|
16
|
+
@_read
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :params, :path
|
|
22
|
+
|
|
23
|
+
def text = File.read(path)
|
|
24
|
+
|
|
25
|
+
def data = @_data ||= YAML.safe_load(text)
|
|
26
|
+
|
|
27
|
+
def read_for(p)
|
|
28
|
+
data[p.name].tap do |value|
|
|
29
|
+
p.multi? ? validate_types!(p, value) : validate_type!(p, value)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_type!(p, value)
|
|
34
|
+
case p.type
|
|
35
|
+
when "string" then validate_string!(p.name, value)
|
|
36
|
+
when "boolean" then validate_boolean!(p.name, value)
|
|
37
|
+
when "integer" then validate_integer!(p.name, value)
|
|
38
|
+
when "float" then validate_float!(p.name, value)
|
|
39
|
+
else
|
|
40
|
+
raise Invalid, "Unfig::FileLoader does not know how to handle the parameter type '#{p.type}'"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_types!(p, value)
|
|
45
|
+
if value.is_a?(Array)
|
|
46
|
+
value.each { |v| validate_type!(p, v) }
|
|
47
|
+
else
|
|
48
|
+
raise(InvalidYamlValue, "FileLoader expected an array for #{p.name}, because it's multi-valued")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_string!(name, value)
|
|
53
|
+
return if value.nil? || value.is_a?(String)
|
|
54
|
+
|
|
55
|
+
raise InvalidYamlValue, "Unfig::FileLoader expected a string for #{name}, but got #{value.class}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_boolean!(name, value)
|
|
59
|
+
return if [nil, true, false].include?(value)
|
|
60
|
+
|
|
61
|
+
raise InvalidYamlValue, "Unfig::FileLoader expected a boolean for #{name}, but got #{value.class}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_integer!(name, value)
|
|
65
|
+
return if value.nil? || value.is_a?(Integer)
|
|
66
|
+
|
|
67
|
+
raise InvalidYamlValue, "Unfig::FileLoader expected an integer for #{name}, but got #{value.class}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_float!(name, value)
|
|
71
|
+
return if value.nil? || value.is_a?(Numeric)
|
|
72
|
+
|
|
73
|
+
raise InvalidYamlValue, "Unfig::FileLoader expected a float for #{name}, but got #{value.class}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/unfig/loader.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class Loader
|
|
3
|
+
def initialize(values:, argv: UNSUPPLIED, env: UNSUPPLIED, config: UNSUPPLIED, **options)
|
|
4
|
+
@argv = argv
|
|
5
|
+
@env = env
|
|
6
|
+
@config = config
|
|
7
|
+
@values = values
|
|
8
|
+
@format = options.fetch(:format, :hash)
|
|
9
|
+
@banner = options.fetch(:banner, nil)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read
|
|
13
|
+
@_read ||=
|
|
14
|
+
case format.to_s
|
|
15
|
+
when "hash" then hashed_values
|
|
16
|
+
when "struct" then struct_values
|
|
17
|
+
when "openstruct" then openstruct_values
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError, "Unfig::Loader cannot return results in the format '#{format}'"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :config, :values, :format, :banner
|
|
26
|
+
|
|
27
|
+
def argv = (@argv == UNSUPPLIED) ? ARGV : @argv
|
|
28
|
+
|
|
29
|
+
def env = (@env == UNSUPPLIED) ? ENV.to_h : @env
|
|
30
|
+
|
|
31
|
+
def params = @_params ||= ParamsConfig.new(banner:, params: values)
|
|
32
|
+
|
|
33
|
+
def loaded_argv
|
|
34
|
+
return @_loaded_argv if defined?(@_loaded_argv)
|
|
35
|
+
@_loaded_argv = argv.nil? ? {} : ArgvLoader.new(params:, argv:).read
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def loaded_env
|
|
39
|
+
return @_loaded_env if defined?(@_loaded_env)
|
|
40
|
+
@_loaded_env = env.nil? ? {} : EnvLoader.new(params:, env:).read
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def loaded_file
|
|
44
|
+
return @_loaded_file if defined?(@_loaded_file)
|
|
45
|
+
@_loaded_file = (config == UNSUPPLIED || config.nil?) ? {} : FileLoader.new(params:, path: config).read
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def defaults = @_defaults ||= params.params.map { |p| [p.name, p.default] }.to_h
|
|
49
|
+
|
|
50
|
+
def merged_values = @_merged_values ||= defaults.merge(loaded_file).merge(loaded_env).merge(loaded_argv)
|
|
51
|
+
|
|
52
|
+
def hashed_values = merged_values.transform_keys(&:to_sym)
|
|
53
|
+
|
|
54
|
+
def struct_values
|
|
55
|
+
struct_keys = params.params.map(&:name).map(&:to_sym)
|
|
56
|
+
struct = Struct.new(*struct_keys, keyword_init: true)
|
|
57
|
+
struct.new(**merged_values)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def openstruct_values
|
|
61
|
+
# Don't require ostruct unless they try to use it (we don't want to make it one of _our_
|
|
62
|
+
# dependencies, but if they try to use it without making it one of _theirs_, ruby will
|
|
63
|
+
# inform them)
|
|
64
|
+
require "ostruct"
|
|
65
|
+
OpenStruct.new(merged_values)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
# ParamConfig wraps the configuration of an individual param value, and all of
|
|
3
|
+
# the ways it can be supplied (a long flag, a short flag, a config-file entry,
|
|
4
|
+
# or an environment variable).
|
|
5
|
+
#
|
|
6
|
+
# It enforces the allowed values on each piece of configuration, constructs
|
|
7
|
+
# defaults for several of them (based on the name), and exposes which of those
|
|
8
|
+
# methods are _enabled_ for that variable (you can decide that `--verbose` is
|
|
9
|
+
# not suppliable through the config file for example). Each parameter can also
|
|
10
|
+
# be designated as `multi?`, meaning that it can be supplied multiple times
|
|
11
|
+
# (or supplied as an array, in the config-file case), and produces an array of
|
|
12
|
+
# values in any case.
|
|
13
|
+
class ParamConfig
|
|
14
|
+
KNOWN_ENABLED_VALUES = ["long", "short", "env", "file"].to_set.freeze
|
|
15
|
+
|
|
16
|
+
def self.load(params_data) = params_data.map { |key, value| new(key, value) }
|
|
17
|
+
|
|
18
|
+
def initialize(name, data)
|
|
19
|
+
@name = name
|
|
20
|
+
@data = data.transform_keys(&:to_sym)
|
|
21
|
+
ParamValidator.new(@name, @data).validate!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def name = @name.to_s
|
|
25
|
+
|
|
26
|
+
def description = data.fetch(:description)
|
|
27
|
+
|
|
28
|
+
def type = data.fetch(:type)
|
|
29
|
+
|
|
30
|
+
def default = data.fetch(:default)
|
|
31
|
+
|
|
32
|
+
def multi? = data.fetch(:multi, false)
|
|
33
|
+
|
|
34
|
+
def enabled
|
|
35
|
+
if data.key?(:enabled)
|
|
36
|
+
data.fetch(:enabled)
|
|
37
|
+
else
|
|
38
|
+
KNOWN_ENABLED_VALUES.to_a.sort
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def long
|
|
43
|
+
if data.key?(:long)
|
|
44
|
+
data.fetch(:long)
|
|
45
|
+
else
|
|
46
|
+
name.tr("_", "-").downcase
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def short
|
|
51
|
+
if data.key?(:short)
|
|
52
|
+
data.fetch(:short)
|
|
53
|
+
else
|
|
54
|
+
name[0]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def env
|
|
59
|
+
if data.key?(:env)
|
|
60
|
+
data.fetch(:env, nil)
|
|
61
|
+
else
|
|
62
|
+
name.upcase
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :data
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
class ParamValidator
|
|
3
|
+
MAX_NAME = 64
|
|
4
|
+
MAX_LONG_FLAG = 64
|
|
5
|
+
MAX_ENV_LENGTH = 64
|
|
6
|
+
KNOWN_TYPES = ["boolean", "string", "integer", "float"].to_set.freeze
|
|
7
|
+
KNOWN_ENABLEMENTS = ParamConfig::KNOWN_ENABLED_VALUES.dup.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(name, data)
|
|
10
|
+
@name = name
|
|
11
|
+
@data = data
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate!
|
|
15
|
+
validate_name!
|
|
16
|
+
validate_description!
|
|
17
|
+
validate_type!
|
|
18
|
+
validate_multi!
|
|
19
|
+
validate_enabled!
|
|
20
|
+
validate_default!
|
|
21
|
+
validate_long!
|
|
22
|
+
validate_short!
|
|
23
|
+
validate_env!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :name, :data
|
|
29
|
+
|
|
30
|
+
# accessors
|
|
31
|
+
|
|
32
|
+
[:description, :type, :multi, :enabled, :default, :long, :short, :env].each do |key|
|
|
33
|
+
define_method(key) { data[key] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# helpers
|
|
37
|
+
|
|
38
|
+
def invalid!(msg) = raise(Invalid, "Param '#{name}': #{msg}")
|
|
39
|
+
|
|
40
|
+
def missing?(key) = !data.key?(key)
|
|
41
|
+
|
|
42
|
+
def nonstring?(s) = !s.is_a?(String)
|
|
43
|
+
|
|
44
|
+
def nonstringlike?(s) = !(s.is_a?(String) || s.is_a?(Symbol))
|
|
45
|
+
|
|
46
|
+
def blank?(s) = !/\S/.match?(s)
|
|
47
|
+
|
|
48
|
+
def whitespace?(s) = /\s/.match?(s)
|
|
49
|
+
|
|
50
|
+
def multi_line?(s) = /\n/.match?(s)
|
|
51
|
+
|
|
52
|
+
def alphanumeric?(s) = /\A[a-zA-Z0-9_]+\z/.match?(s)
|
|
53
|
+
|
|
54
|
+
def boolean?(v) = [true, false].include?(v)
|
|
55
|
+
|
|
56
|
+
def array?(v) = v.is_a?(Array)
|
|
57
|
+
|
|
58
|
+
def known_type?(t) = KNOWN_TYPES.include?(t)
|
|
59
|
+
|
|
60
|
+
def known_enablement?(e) = KNOWN_ENABLEMENTS.include?(e)
|
|
61
|
+
|
|
62
|
+
def unrecognized_enablements = enabled.reject { |e| known_enablement?(e) }.sort
|
|
63
|
+
|
|
64
|
+
def types_list = KNOWN_TYPES.to_a.sort.map(&:to_s).join(", ")
|
|
65
|
+
|
|
66
|
+
# validators
|
|
67
|
+
|
|
68
|
+
def validate_name!
|
|
69
|
+
invalid!("Name is not a string") if nonstringlike?(name)
|
|
70
|
+
invalid!("Name may contain only alphanumerics and underscores") unless alphanumeric?(name)
|
|
71
|
+
invalid!("Name contains more than #{MAX_NAME} characters") if name.length > MAX_NAME
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_description!
|
|
75
|
+
invalid!("Description must be supplied") if missing?(:description)
|
|
76
|
+
invalid!("Description must be supplied as a string") if nonstring?(description)
|
|
77
|
+
invalid!("Description must not be blank") if blank?(description)
|
|
78
|
+
invalid!("Description may not include newlines") if multi_line?(description)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_type!
|
|
82
|
+
invalid!("Type was not supplied") if missing?(:type)
|
|
83
|
+
invalid!("Type must be supplied as a string") if nonstring?(type)
|
|
84
|
+
invalid!("Type not recognized - expected #{types_list}") unless known_type?(type)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_multi!
|
|
88
|
+
return unless data.key?(:multi)
|
|
89
|
+
|
|
90
|
+
invalid!("Multi must be a boolean") unless boolean?(multi)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_enabled!
|
|
94
|
+
return if missing?(:enabled)
|
|
95
|
+
|
|
96
|
+
invalid!("Enabled must be an array") unless array?(enabled)
|
|
97
|
+
invalid!("Enabled must not be empty") if enabled.empty?
|
|
98
|
+
|
|
99
|
+
if unrecognized_enablements.any?
|
|
100
|
+
invalid!("Enabled includes unrecognized values: #{unrecognized_enablements.join(", ")}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate_multi_default!
|
|
105
|
+
invalid!("Multi-valued, but default is not an Array") unless array?(default)
|
|
106
|
+
|
|
107
|
+
if default.any? { |entry| !correct_default_type?(type, entry) }
|
|
108
|
+
invalid!("Default includes non-#{type} values")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_single_default!
|
|
113
|
+
return if correct_default_type?(type, default)
|
|
114
|
+
invalid!("Default is not a #{type}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def validate_default!
|
|
118
|
+
invalid!("Default not supplied") if missing?(:default)
|
|
119
|
+
return if default.nil?
|
|
120
|
+
|
|
121
|
+
multi ? validate_multi_default! : validate_single_default!
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def correct_default_type?(type, value)
|
|
125
|
+
case type
|
|
126
|
+
when "boolean" then [true, false].include?(value)
|
|
127
|
+
when "string" then value.is_a?(String)
|
|
128
|
+
when "integer" then value.is_a?(Integer)
|
|
129
|
+
when "float" then value.is_a?(Numeric)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_long!
|
|
134
|
+
return if missing?(:long)
|
|
135
|
+
|
|
136
|
+
invalid!("Long flag is not a string") if nonstring?(long)
|
|
137
|
+
invalid!("Long flag includes whitespace") if whitespace?(long)
|
|
138
|
+
invalid!("Long flag is over #{MAX_LONG_FLAG} characters") if long.length > MAX_LONG_FLAG
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_short!
|
|
142
|
+
return if missing?(:short)
|
|
143
|
+
|
|
144
|
+
invalid!("Short flag is not a string") if nonstring?(short)
|
|
145
|
+
invalid!("Short flag must be a single letter or digit") unless /\A[a-z0-9]\z/i.match?(short)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_env!
|
|
149
|
+
return if missing?(:env)
|
|
150
|
+
|
|
151
|
+
invalid!("ENV name is not a string") if nonstring?(env)
|
|
152
|
+
invalid!("ENV name may only contain alphanumerics and underscores") unless alphanumeric?(env)
|
|
153
|
+
invalid!("ENV name must begin with a letter") unless /\A[a-z]/i.match?(env)
|
|
154
|
+
invalid!("ENV name is over #{MAX_ENV_LENGTH} characters") if env.length > MAX_ENV_LENGTH
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Unfig
|
|
2
|
+
# ParamsConfig wraps the configuration of _all_ of the possible parameters,
|
|
3
|
+
# and enforces uniqueness validations - you can't have two parameters with
|
|
4
|
+
# the same flag, or the same name; they'll collide in the implementation.
|
|
5
|
+
class ParamsConfig
|
|
6
|
+
def initialize(data)
|
|
7
|
+
raise(Invalid, "Params-config must be a Hash") unless data.is_a?(Hash)
|
|
8
|
+
raise(Invalid, "Params-config must supply some params (as a Hash)") unless data[:params].is_a?(Hash)
|
|
9
|
+
@data = data
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def params
|
|
13
|
+
return @_params if defined?(@_params)
|
|
14
|
+
validate!
|
|
15
|
+
@_params = built
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def banner = data[:banner]
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :data
|
|
23
|
+
|
|
24
|
+
def built = @_built ||= data[:params].map { |k, v| ParamConfig.new(k, v) }
|
|
25
|
+
|
|
26
|
+
def validate!
|
|
27
|
+
validate_no_duplicate_names!
|
|
28
|
+
validate_no_duplicate_long_flags!
|
|
29
|
+
validate_no_duplicate_short_flags!
|
|
30
|
+
validate_no_duplicate_envs!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def repeats(items) = items.tally.select { |_k, v| v > 1 }.keys.sort
|
|
34
|
+
|
|
35
|
+
def validate_no_duplicate_names!
|
|
36
|
+
dups = repeats(built.map(&:name))
|
|
37
|
+
return if dups.none?
|
|
38
|
+
|
|
39
|
+
raise Invalid, "Duplicate parameter names: #{dups.join(", ")}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_no_duplicate_long_flags!
|
|
43
|
+
dups = repeats(built.map(&:long))
|
|
44
|
+
return if dups.none?
|
|
45
|
+
|
|
46
|
+
raise Invalid, "Duplicate long-flags: #{dups.join(", ")}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_no_duplicate_short_flags!
|
|
50
|
+
dups = repeats(built.map(&:short))
|
|
51
|
+
return if dups.none?
|
|
52
|
+
|
|
53
|
+
raise Invalid, "Duplicate short-flags: #{dups.join(", ")}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_no_duplicate_envs!
|
|
57
|
+
dups = repeats(built.map(&:env))
|
|
58
|
+
return if dups.none?
|
|
59
|
+
|
|
60
|
+
raise Invalid, "Duplicate env-names: #{dups.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/unfig.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "optparse"
|
|
3
|
+
|
|
4
|
+
module Unfig
|
|
5
|
+
UNSUPPLIED = Object.new.freeze
|
|
6
|
+
|
|
7
|
+
Error = Class.new(StandardError)
|
|
8
|
+
Invalid = Class.new(Error)
|
|
9
|
+
|
|
10
|
+
InvalidEnv = Class.new(Error)
|
|
11
|
+
InvalidBooleanText = Class.new(InvalidEnv)
|
|
12
|
+
InvalidIntegerText = Class.new(InvalidEnv)
|
|
13
|
+
InvalidFloatingPointText = Class.new(InvalidEnv)
|
|
14
|
+
|
|
15
|
+
InvalidYamlValue = Class.new(Error)
|
|
16
|
+
FlagError = Class.new(Error)
|
|
17
|
+
|
|
18
|
+
def self.load_options(**) = Loader.new(**).read
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative "unfig/version"
|
|
22
|
+
require_relative "unfig/param_config"
|
|
23
|
+
require_relative "unfig/param_validator"
|
|
24
|
+
require_relative "unfig/params_config"
|
|
25
|
+
require_relative "unfig/env_reader"
|
|
26
|
+
require_relative "unfig/env_loader"
|
|
27
|
+
require_relative "unfig/file_loader"
|
|
28
|
+
require_relative "unfig/argv_loader"
|
|
29
|
+
require_relative "unfig/loader"
|
data/unfig.gemspec
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative "lib/unfig/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "unfig"
|
|
5
|
+
spec.version = Unfig::VERSION
|
|
6
|
+
spec.authors = ["Eric Mueller"]
|
|
7
|
+
spec.email = ["nevinera@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "Build CLIs that are configured via args, file, and/or environment"
|
|
10
|
+
spec.description = <<~DESC
|
|
11
|
+
We've written code that merges/cascades default configuration, config-files,
|
|
12
|
+
environment variables, and cli-passed arguments _too many times_. This gem
|
|
13
|
+
intends to distill that into a configuration config-file describing those
|
|
14
|
+
controls and relationships.
|
|
15
|
+
DESC
|
|
16
|
+
spec.homepage = "https://github.com/nevinera/unfig"
|
|
17
|
+
spec.license = "MIT"
|
|
18
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
19
|
+
|
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
22
|
+
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
spec.bindir = "bin"
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
`git ls-files -z`
|
|
27
|
+
.split("\x0")
|
|
28
|
+
.reject { |f| f.start_with?("spec") }
|
|
29
|
+
end
|
|
30
|
+
spec.executables = Dir.chdir(File.expand_path(__dir__)) do
|
|
31
|
+
`git ls-files -z bin/`
|
|
32
|
+
.split("\x0")
|
|
33
|
+
.map { |path| path.sub(/^bin\//, "") }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
37
|
+
spec.add_development_dependency "rspec-its", "~> 1.3"
|
|
38
|
+
spec.add_development_dependency "rspec-collection_matchers", "~> 1.2.1"
|
|
39
|
+
spec.add_development_dependency "simplecov", "~> 0.22.0"
|
|
40
|
+
spec.add_development_dependency "pry", "~> 0.14"
|
|
41
|
+
spec.add_development_dependency "standard", ">= 1.35.1"
|
|
42
|
+
spec.add_development_dependency "rubocop", ">= 1.62"
|
|
43
|
+
spec.add_development_dependency "mdl", "~> 0.12"
|
|
44
|
+
spec.add_development_dependency "ostruct"
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: unfig
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Eric Mueller
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.13'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec-its
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.3'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec-collection_matchers
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 1.2.1
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.2.1
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: simplecov
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.22.0
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 0.22.0
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: pry
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.14'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.14'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: standard
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.35.1
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 1.35.1
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '1.62'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '1.62'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: mdl
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.12'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.12'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: ostruct
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
description: |
|
|
140
|
+
We've written code that merges/cascades default configuration, config-files,
|
|
141
|
+
environment variables, and cli-passed arguments _too many times_. This gem
|
|
142
|
+
intends to distill that into a configuration config-file describing those
|
|
143
|
+
controls and relationships.
|
|
144
|
+
email:
|
|
145
|
+
- nevinera@gmail.com
|
|
146
|
+
executables: []
|
|
147
|
+
extensions: []
|
|
148
|
+
extra_rdoc_files: []
|
|
149
|
+
files:
|
|
150
|
+
- ".github/workflows/linters.yml"
|
|
151
|
+
- ".github/workflows/rspec.yml"
|
|
152
|
+
- ".gitignore"
|
|
153
|
+
- ".mdl_rules.rb"
|
|
154
|
+
- ".mdlrc"
|
|
155
|
+
- ".rspec"
|
|
156
|
+
- ".rubocop.yml"
|
|
157
|
+
- ".standard.yml"
|
|
158
|
+
- Gemfile
|
|
159
|
+
- README.md
|
|
160
|
+
- lib/unfig.rb
|
|
161
|
+
- lib/unfig/argv_loader.rb
|
|
162
|
+
- lib/unfig/env_loader.rb
|
|
163
|
+
- lib/unfig/env_reader.rb
|
|
164
|
+
- lib/unfig/file_loader.rb
|
|
165
|
+
- lib/unfig/loader.rb
|
|
166
|
+
- lib/unfig/param_config.rb
|
|
167
|
+
- lib/unfig/param_validator.rb
|
|
168
|
+
- lib/unfig/params_config.rb
|
|
169
|
+
- lib/unfig/version.rb
|
|
170
|
+
- unfig.gemspec
|
|
171
|
+
homepage: https://github.com/nevinera/unfig
|
|
172
|
+
licenses:
|
|
173
|
+
- MIT
|
|
174
|
+
metadata:
|
|
175
|
+
homepage_uri: https://github.com/nevinera/unfig
|
|
176
|
+
source_code_uri: https://github.com/nevinera/unfig
|
|
177
|
+
post_install_message:
|
|
178
|
+
rdoc_options: []
|
|
179
|
+
require_paths:
|
|
180
|
+
- lib
|
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
182
|
+
requirements:
|
|
183
|
+
- - ">="
|
|
184
|
+
- !ruby/object:Gem::Version
|
|
185
|
+
version: 3.2.0
|
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
|
+
requirements:
|
|
188
|
+
- - ">="
|
|
189
|
+
- !ruby/object:Gem::Version
|
|
190
|
+
version: '0'
|
|
191
|
+
requirements: []
|
|
192
|
+
rubygems_version: 3.4.19
|
|
193
|
+
signing_key:
|
|
194
|
+
specification_version: 4
|
|
195
|
+
summary: Build CLIs that are configured via args, file, and/or environment
|
|
196
|
+
test_files: []
|