dry-configurable 0.7.0 → 0.9.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 +5 -5
- data/.codeclimate.yml +10 -21
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/ci.yml +70 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +30 -0
- data/.gitignore +1 -1
- data/.rspec +3 -1
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +87 -6
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +4 -0
- data/LICENSE +1 -1
- data/README.md +20 -32
- data/docsite/source/index.html.md +55 -0
- data/docsite/source/testing.html.md +27 -0
- data/dry-configurable.gemspec +10 -4
- data/lib/dry/configurable.rb +124 -118
- data/lib/dry/configurable/config.rb +136 -43
- data/lib/dry/configurable/error.rb +0 -2
- data/lib/dry/configurable/setting.rb +46 -0
- data/lib/dry/configurable/settings.rb +117 -0
- data/lib/dry/configurable/settings/argument_parser.rb +50 -0
- data/lib/dry/configurable/test_interface.rb +5 -1
- data/lib/dry/configurable/version.rb +1 -1
- metadata +56 -38
- data/.ruby-version +0 -1
- data/.travis.yml +0 -34
- data/lib/dry/configurable/argument_parser.rb +0 -91
- data/lib/dry/configurable/config/value.rb +0 -27
- data/lib/dry/configurable/nested_config.rb +0 -33
- data/spec/integration/configurable_spec.rb +0 -25
- data/spec/spec_helper.rb +0 -92
- data/spec/support/shared_examples/configurable.rb +0 -391
- data/spec/unit/dry/configurable/argument_parser_spec.rb +0 -114
- data/spec/unit/dry/configurable/config/value_spec.rb +0 -55
- data/spec/unit/dry/configurable/config_spec.rb +0 -186
@@ -1,61 +1,89 @@
|
|
1
|
+
require 'concurrent/hash'
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Configurable
|
3
5
|
# @private
|
4
6
|
class Config
|
5
|
-
|
7
|
+
class << self
|
8
|
+
# @private
|
9
|
+
def [](settings)
|
10
|
+
::Class.new(Config) do
|
11
|
+
@settings = settings
|
12
|
+
singleton_class.send(:attr_reader, :settings)
|
13
|
+
|
14
|
+
@lock = ::Mutex.new
|
15
|
+
@config_defined = false
|
16
|
+
end
|
17
|
+
end
|
6
18
|
|
7
|
-
|
8
|
-
|
19
|
+
# @private
|
20
|
+
def define_accessors!
|
21
|
+
@lock.synchronize do
|
22
|
+
break if config_defined?
|
9
23
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
24
|
+
settings.each do |setting|
|
25
|
+
next if setting.reserved?
|
26
|
+
|
27
|
+
define_method(setting.name) do
|
28
|
+
@config[setting.name]
|
29
|
+
end
|
14
30
|
|
15
|
-
|
16
|
-
|
17
|
-
|
31
|
+
define_method("#{setting.name}=") do |value|
|
32
|
+
raise FrozenConfig, 'Cannot modify frozen config' if frozen?
|
33
|
+
@config[setting.name] = setting.processor.(value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@config_defined = true
|
18
38
|
end
|
19
39
|
end
|
20
40
|
|
21
|
-
|
41
|
+
# @private
|
42
|
+
def config_defined?
|
43
|
+
@config_defined
|
44
|
+
end
|
22
45
|
end
|
23
46
|
|
24
|
-
def initialize
|
47
|
+
def initialize
|
25
48
|
@config = ::Concurrent::Hash.new
|
26
|
-
|
27
|
-
|
28
|
-
if setting.none?
|
29
|
-
@config[setting.name] = nil
|
30
|
-
else
|
31
|
-
public_send("#{setting.name}=", setting.value)
|
32
|
-
end
|
33
|
-
end
|
49
|
+
@lock = ::Mutex.new
|
50
|
+
@defined = false
|
34
51
|
end
|
35
52
|
|
36
|
-
def
|
37
|
-
|
38
|
-
dup.instance_variable_set(:@config, @config.dup)
|
39
|
-
dup
|
53
|
+
def defined?
|
54
|
+
@defined
|
40
55
|
end
|
41
56
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
57
|
+
# @private
|
58
|
+
def define!(parent_config = EMPTY_HASH)
|
59
|
+
@lock.synchronize do
|
60
|
+
break if self.defined?
|
61
|
+
|
62
|
+
self.class.define_accessors!
|
63
|
+
set_values!(parent_config)
|
64
|
+
|
65
|
+
@defined = true
|
66
|
+
end
|
67
|
+
|
68
|
+
self
|
46
69
|
end
|
47
70
|
|
71
|
+
# @private
|
48
72
|
def finalize!
|
73
|
+
define!
|
49
74
|
@config.freeze
|
50
75
|
freeze
|
51
76
|
end
|
52
77
|
|
78
|
+
# Serialize config to a Hash
|
79
|
+
#
|
80
|
+
# @return [Hash]
|
81
|
+
#
|
82
|
+
# @api public
|
53
83
|
def to_h
|
54
|
-
@config.each_with_object({}) do |
|
55
|
-
key, value = tuple
|
56
|
-
|
84
|
+
@config.each_with_object({}) do |(key, value), hash|
|
57
85
|
case value
|
58
|
-
when
|
86
|
+
when Config
|
59
87
|
hash[key] = value.to_h
|
60
88
|
else
|
61
89
|
hash[key] = value
|
@@ -64,28 +92,93 @@ module Dry
|
|
64
92
|
end
|
65
93
|
alias to_hash to_h
|
66
94
|
|
95
|
+
# Get config value by a key
|
96
|
+
#
|
97
|
+
# @param [String,Symbol] name
|
98
|
+
#
|
99
|
+
# @return Config value
|
67
100
|
def [](name)
|
68
|
-
|
69
|
-
|
101
|
+
setting = self.class.settings[name.to_sym]
|
102
|
+
|
103
|
+
if setting.nil?
|
104
|
+
raise_unknown_setting_error(name)
|
105
|
+
elsif setting.reserved?
|
106
|
+
@config[setting.name]
|
107
|
+
else
|
108
|
+
public_send(name)
|
109
|
+
end
|
70
110
|
end
|
71
111
|
|
112
|
+
# Set config value.
|
113
|
+
# Note that finalized configs cannot be changed.
|
114
|
+
#
|
115
|
+
# @param [String,Symbol] name
|
116
|
+
# @param [Object] value
|
72
117
|
def []=(name, value)
|
73
|
-
|
74
|
-
|
118
|
+
setting = self.class.settings[name.to_sym]
|
119
|
+
|
120
|
+
if setting.nil?
|
121
|
+
raise_unknown_setting_error(name)
|
122
|
+
elsif setting.reserved?
|
123
|
+
@config[setting.name] = setting.processor.(value)
|
124
|
+
else
|
125
|
+
public_send("#{name}=", value)
|
126
|
+
end
|
75
127
|
end
|
76
128
|
|
77
|
-
|
129
|
+
# Whether config has a key
|
130
|
+
#
|
131
|
+
# @param [Symbol] key
|
132
|
+
# @return [Bool]
|
133
|
+
def key?(name)
|
134
|
+
self.class.settings.name?(name)
|
135
|
+
end
|
78
136
|
|
79
|
-
|
80
|
-
|
137
|
+
# Recursively update values from a hash
|
138
|
+
#
|
139
|
+
# @param [Hash] values to set
|
140
|
+
# @return [Config]
|
141
|
+
def update(values)
|
142
|
+
values.each do |key, value|
|
143
|
+
if self[key].is_a?(Config)
|
144
|
+
self[key].update(value)
|
145
|
+
else
|
146
|
+
self[key] = value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
self
|
81
150
|
end
|
82
151
|
|
83
|
-
def
|
84
|
-
|
152
|
+
def dup
|
153
|
+
if self.defined?
|
154
|
+
self.class.new.define!(to_h)
|
155
|
+
else
|
156
|
+
self.class.new
|
157
|
+
end
|
85
158
|
end
|
86
159
|
|
87
|
-
|
88
|
-
|
160
|
+
private
|
161
|
+
|
162
|
+
# @private
|
163
|
+
def set_values!(parent)
|
164
|
+
self.class.settings.each do |setting|
|
165
|
+
if parent.key?(setting.name) && !setting.node?
|
166
|
+
@config[setting.name] = parent[setting.name]
|
167
|
+
elsif setting.undefined?
|
168
|
+
@config[setting.name] = nil
|
169
|
+
elsif setting.node?
|
170
|
+
value = setting.value.create_config
|
171
|
+
value.define!(parent.fetch(setting.name, EMPTY_HASH))
|
172
|
+
self[setting.name] = value
|
173
|
+
else
|
174
|
+
self[setting.name] = setting.value
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# @private
|
180
|
+
def raise_unknown_setting_error(name)
|
181
|
+
::Kernel.raise ArgumentError, "+#{name}+ is not a setting name"
|
89
182
|
end
|
90
183
|
end
|
91
184
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dry
|
2
|
+
module Configurable
|
3
|
+
# This class represents a setting and is used internally.
|
4
|
+
#
|
5
|
+
# @private
|
6
|
+
class Setting
|
7
|
+
VALID_NAME = /\A[a-z_]\w*\z/i
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
attr_reader :processor
|
14
|
+
|
15
|
+
def initialize(name, value, processor, options = EMPTY_HASH)
|
16
|
+
unless VALID_NAME =~ name.to_s
|
17
|
+
raise ArgumentError, "+#{name}+ is not a valid setting name"
|
18
|
+
end
|
19
|
+
@name = name.to_sym
|
20
|
+
@value = value
|
21
|
+
@processor = processor
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
Undefined.default(@value, nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
def undefined?
|
30
|
+
Undefined.equal?(@value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def reader?
|
34
|
+
options[:reader]
|
35
|
+
end
|
36
|
+
|
37
|
+
def node?
|
38
|
+
Settings === @value
|
39
|
+
end
|
40
|
+
|
41
|
+
def reserved?
|
42
|
+
options[:reserved]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'concurrent/array'
|
3
|
+
require 'dry/configurable/settings/argument_parser'
|
4
|
+
require 'dry/configurable/setting'
|
5
|
+
require 'dry/configurable/config'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Configurable
|
9
|
+
# A collection of settings. This is not part of the public API.
|
10
|
+
#
|
11
|
+
# @private
|
12
|
+
class Settings
|
13
|
+
Parser = ArgumentParser.new.freeze
|
14
|
+
|
15
|
+
class DSL
|
16
|
+
def self.call(&block)
|
17
|
+
new.instance_exec do
|
18
|
+
instance_exec(&block)
|
19
|
+
@settings
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@settings = Settings.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def setting(*args, &block)
|
28
|
+
@settings.add(*args, &block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Capture nested config definition
|
33
|
+
#
|
34
|
+
# @return [Dry::Configurable::Setting]
|
35
|
+
def self.capture(&block)
|
36
|
+
DSL.(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :settings
|
40
|
+
|
41
|
+
attr_reader :config_class
|
42
|
+
|
43
|
+
attr_reader :index
|
44
|
+
private :index
|
45
|
+
|
46
|
+
def initialize(settings = ::Concurrent::Array.new)
|
47
|
+
@settings = settings
|
48
|
+
@config_class = Config[self]
|
49
|
+
@index = settings.map { |s| [s.name, s] }.to_h
|
50
|
+
yield(self) if block_given?
|
51
|
+
end
|
52
|
+
|
53
|
+
def add(key, value = Undefined, options = Undefined, &block)
|
54
|
+
extended = singleton_class < Configurable
|
55
|
+
raise_already_defined_config(key) if extended && configured?
|
56
|
+
|
57
|
+
*args, opts = Parser.(value, options, block)
|
58
|
+
|
59
|
+
Setting.new(key, *args, { **opts, reserved: reserved?(key) }).tap do |s|
|
60
|
+
settings.delete_if { |e| e.name.eql?(s.name) }
|
61
|
+
settings << s
|
62
|
+
index[s.name] = s
|
63
|
+
@names = nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def each
|
68
|
+
settings.each { |s| yield(s) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def names
|
72
|
+
@names ||= index.keys.to_set
|
73
|
+
end
|
74
|
+
|
75
|
+
def [](name)
|
76
|
+
index[name]
|
77
|
+
end
|
78
|
+
|
79
|
+
def empty?
|
80
|
+
settings.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
def name?(name)
|
84
|
+
index.key?(name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def dup
|
88
|
+
Settings.new(settings.dup)
|
89
|
+
end
|
90
|
+
|
91
|
+
def freeze
|
92
|
+
settings.freeze
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_config
|
97
|
+
config_class.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def config_defined?
|
101
|
+
config_class.config_defined?
|
102
|
+
end
|
103
|
+
|
104
|
+
def reserved?(name)
|
105
|
+
reserved_names.include?(name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def reserved_names
|
109
|
+
@reserved_names ||= [
|
110
|
+
config_class.instance_methods(false),
|
111
|
+
config_class.superclass.instance_methods(false),
|
112
|
+
%i(class public_send)
|
113
|
+
].reduce(:+)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Dry
|
2
|
+
# Argument parser
|
3
|
+
#
|
4
|
+
# Passing and array or arguments, it will decide which one are arguments
|
5
|
+
# and which one are options.
|
6
|
+
#
|
7
|
+
# We have a limitation if setting the value without options, as a hash
|
8
|
+
# having the same key as one of the valid options, will parse the value
|
9
|
+
# as options. In this case, all unknown options will be reject with an exception.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# p = Dry::Configurable::ArgumentParser.new.('db:sqlite', reader: true)
|
13
|
+
#
|
14
|
+
# p[0] # => 'db:sqlite'
|
15
|
+
# p[1] # => ArgumentParser::DEFAULT_PROCESSOR
|
16
|
+
# p[2] # => { reader: true }
|
17
|
+
module Configurable
|
18
|
+
class Settings
|
19
|
+
# @private
|
20
|
+
class ArgumentParser
|
21
|
+
DEFAULT_PROCESSOR = ->(v) { v }
|
22
|
+
|
23
|
+
# @private
|
24
|
+
def call(val, opts, block)
|
25
|
+
if block && block.parameters.empty?
|
26
|
+
raise ArgumentError unless Undefined.equal?(opts)
|
27
|
+
|
28
|
+
processor = DEFAULT_PROCESSOR
|
29
|
+
|
30
|
+
value, options = Settings.capture(&block), val
|
31
|
+
else
|
32
|
+
processor = block || DEFAULT_PROCESSOR
|
33
|
+
|
34
|
+
if Undefined.equal?(opts) && val.is_a?(Hash) && val.key?(:reader)
|
35
|
+
value, options = Undefined, val
|
36
|
+
else
|
37
|
+
value, options = val, opts
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
[value, processor, options(**Undefined.default(options, EMPTY_HASH))]
|
42
|
+
end
|
43
|
+
|
44
|
+
def options(reader: false)
|
45
|
+
{ reader: reader }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|