dry-configurable 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|