dry-configurable 0.9.0 → 0.11.3
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 +4 -4
- data/CHANGELOG.md +81 -21
- data/LICENSE +1 -1
- data/README.md +15 -27
- data/dry-configurable.gemspec +27 -18
- data/lib/dry-configurable.rb +2 -0
- data/lib/dry/configurable.rb +21 -146
- data/lib/dry/configurable/class_methods.rb +103 -0
- data/lib/dry/configurable/compiler.rb +45 -0
- data/lib/dry/configurable/config.rb +79 -136
- data/lib/dry/configurable/constants.rb +12 -0
- data/lib/dry/configurable/dsl.rb +62 -0
- data/lib/dry/configurable/dsl/args.rb +58 -0
- data/lib/dry/configurable/{error.rb → errors.rb} +5 -1
- data/lib/dry/configurable/instance_methods.rb +46 -0
- data/lib/dry/configurable/methods.rb +32 -0
- data/lib/dry/configurable/setting.rb +91 -17
- data/lib/dry/configurable/settings.rb +42 -87
- data/lib/dry/configurable/test_interface.rb +3 -5
- data/lib/dry/configurable/version.rb +3 -1
- metadata +30 -25
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -70
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -30
- data/.gitignore +0 -9
- data/.rspec +0 -4
- data/.rubocop.yml +0 -89
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -20
- data/Rakefile +0 -12
- data/docsite/source/index.html.md +0 -55
- data/docsite/source/testing.html.md +0 -27
- data/lib/dry/configurable/settings/argument_parser.rb +0 -50
- data/rakelib/rubocop.rake +0 -18
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'dry/configurable/constants'
|
6
|
+
require 'dry/configurable/dsl'
|
7
|
+
require 'dry/configurable/methods'
|
8
|
+
require 'dry/configurable/settings'
|
9
|
+
|
10
|
+
module Dry
|
11
|
+
module Configurable
|
12
|
+
module ClassMethods
|
13
|
+
include Methods
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
def inherited(klass)
|
17
|
+
super
|
18
|
+
|
19
|
+
parent_settings = (respond_to?(:config) ? config._settings : _settings)
|
20
|
+
|
21
|
+
klass.instance_variable_set('@_settings', parent_settings)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add a setting to the configuration
|
25
|
+
#
|
26
|
+
# @param [Mixed] key
|
27
|
+
# The accessor key for the configuration value
|
28
|
+
# @param [Mixed] default
|
29
|
+
# The default config value
|
30
|
+
#
|
31
|
+
# @yield
|
32
|
+
# If a block is given, it will be evaluated in the context of
|
33
|
+
# a new configuration class, and bound as the default value
|
34
|
+
#
|
35
|
+
# @return [Dry::Configurable::Config]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def setting(*args, &block)
|
39
|
+
setting = __config_dsl__.setting(*args, &block)
|
40
|
+
|
41
|
+
_settings << setting
|
42
|
+
|
43
|
+
__config_reader__.define(setting.name) if setting.reader?
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return declared settings
|
49
|
+
#
|
50
|
+
# @return [Set<Symbol>]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def settings
|
54
|
+
@settings ||= Set[*_settings.map(&:name)]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return declared settings
|
58
|
+
#
|
59
|
+
# @return [Settings]
|
60
|
+
#
|
61
|
+
# @api public
|
62
|
+
def _settings
|
63
|
+
@_settings ||= Settings.new
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return configuration
|
67
|
+
#
|
68
|
+
# @return [Config]
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def config
|
72
|
+
@config ||= Config.new(_settings)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def __config_dsl__
|
77
|
+
@dsl ||= DSL.new
|
78
|
+
end
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
def __config_reader__
|
82
|
+
@__config_reader__ ||=
|
83
|
+
begin
|
84
|
+
reader = Module.new do
|
85
|
+
def self.define(name)
|
86
|
+
define_method(name) do
|
87
|
+
config[name]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if included_modules.include?(InstanceMethods)
|
93
|
+
include(reader)
|
94
|
+
end
|
95
|
+
|
96
|
+
extend(reader)
|
97
|
+
|
98
|
+
reader
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/configurable/setting'
|
4
|
+
require 'dry/configurable/settings'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Configurable
|
8
|
+
# Setting compiler used internally by the DSL
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Compiler
|
12
|
+
def call(ast)
|
13
|
+
Settings.new.tap do |settings|
|
14
|
+
ast.each do |node|
|
15
|
+
settings << visit(node)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def visit(node)
|
22
|
+
type, rest = node
|
23
|
+
public_send(:"visit_#{type}", rest)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def visit_constructor(node)
|
28
|
+
setting, constructor = node
|
29
|
+
visit(setting).with(constructor: constructor)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def visit_setting(node)
|
34
|
+
name, default, opts = node
|
35
|
+
Setting.new(name, **opts, default: default)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
def visit_nested(node)
|
40
|
+
parent, children = node
|
41
|
+
visit(parent).nested(call(children))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,112 +1,42 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
module Configurable
|
5
|
-
# @private
|
6
|
-
class Config
|
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
|
18
|
-
|
19
|
-
# @private
|
20
|
-
def define_accessors!
|
21
|
-
@lock.synchronize do
|
22
|
-
break if config_defined?
|
23
|
-
|
24
|
-
settings.each do |setting|
|
25
|
-
next if setting.reserved?
|
26
|
-
|
27
|
-
define_method(setting.name) do
|
28
|
-
@config[setting.name]
|
29
|
-
end
|
30
|
-
|
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
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# @private
|
42
|
-
def config_defined?
|
43
|
-
@config_defined
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def initialize
|
48
|
-
@config = ::Concurrent::Hash.new
|
49
|
-
@lock = ::Mutex.new
|
50
|
-
@defined = false
|
51
|
-
end
|
3
|
+
require 'concurrent/map'
|
52
4
|
|
53
|
-
|
54
|
-
@defined
|
55
|
-
end
|
5
|
+
require 'dry/equalizer'
|
56
6
|
|
57
|
-
|
58
|
-
|
59
|
-
@lock.synchronize do
|
60
|
-
break if self.defined?
|
7
|
+
require 'dry/configurable/constants'
|
8
|
+
require 'dry/configurable/errors'
|
61
9
|
|
62
|
-
|
63
|
-
|
10
|
+
module Dry
|
11
|
+
module Configurable
|
12
|
+
# Config exposes setting values through a convenient API
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class Config
|
16
|
+
include Dry::Equalizer(:values)
|
64
17
|
|
65
|
-
|
66
|
-
|
18
|
+
# @api private
|
19
|
+
attr_reader :_settings
|
67
20
|
|
68
|
-
|
69
|
-
|
21
|
+
# @api private
|
22
|
+
attr_reader :_resolved
|
70
23
|
|
71
|
-
# @private
|
72
|
-
def
|
73
|
-
|
74
|
-
@
|
75
|
-
freeze
|
24
|
+
# @api private
|
25
|
+
def initialize(settings)
|
26
|
+
@_settings = settings.dup
|
27
|
+
@_resolved = Concurrent::Map.new
|
76
28
|
end
|
77
29
|
|
78
|
-
# Serialize config to a Hash
|
79
|
-
#
|
80
|
-
# @return [Hash]
|
81
|
-
#
|
82
|
-
# @api public
|
83
|
-
def to_h
|
84
|
-
@config.each_with_object({}) do |(key, value), hash|
|
85
|
-
case value
|
86
|
-
when Config
|
87
|
-
hash[key] = value.to_h
|
88
|
-
else
|
89
|
-
hash[key] = value
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
alias to_hash to_h
|
94
|
-
|
95
30
|
# Get config value by a key
|
96
31
|
#
|
97
32
|
# @param [String,Symbol] name
|
98
33
|
#
|
99
34
|
# @return Config value
|
100
35
|
def [](name)
|
101
|
-
|
36
|
+
name = name.to_sym
|
37
|
+
raise ArgumentError, "+#{name}+ is not a setting name" unless _settings.key?(name)
|
102
38
|
|
103
|
-
|
104
|
-
raise_unknown_setting_error(name)
|
105
|
-
elsif setting.reserved?
|
106
|
-
@config[setting.name]
|
107
|
-
else
|
108
|
-
public_send(name)
|
109
|
-
end
|
39
|
+
_settings[name].value
|
110
40
|
end
|
111
41
|
|
112
42
|
# Set config value.
|
@@ -115,70 +45,83 @@ module Dry
|
|
115
45
|
# @param [String,Symbol] name
|
116
46
|
# @param [Object] value
|
117
47
|
def []=(name, value)
|
118
|
-
|
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
|
48
|
+
public_send(:"#{name}=", value)
|
127
49
|
end
|
128
50
|
|
129
|
-
#
|
51
|
+
# Update config with new values
|
130
52
|
#
|
131
|
-
# @param [
|
132
|
-
# @return [Bool]
|
133
|
-
def key?(name)
|
134
|
-
self.class.settings.name?(name)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Recursively update values from a hash
|
53
|
+
# @param [Hash] A hash with new values
|
138
54
|
#
|
139
|
-
# @param [Hash] values to set
|
140
55
|
# @return [Config]
|
56
|
+
#
|
57
|
+
# @api public
|
141
58
|
def update(values)
|
142
59
|
values.each do |key, value|
|
143
|
-
|
60
|
+
case value
|
61
|
+
when Hash
|
144
62
|
self[key].update(value)
|
145
63
|
else
|
146
64
|
self[key] = value
|
147
65
|
end
|
148
66
|
end
|
149
|
-
self
|
150
67
|
end
|
151
68
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
69
|
+
# Dump config into a hash
|
70
|
+
#
|
71
|
+
# @return [Hash]
|
72
|
+
#
|
73
|
+
# @api public
|
74
|
+
def values
|
75
|
+
_settings
|
76
|
+
.map { |setting| [setting.name, setting.value] }
|
77
|
+
.map { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] }
|
78
|
+
.to_h
|
79
|
+
end
|
80
|
+
alias_method :to_h, :values
|
81
|
+
alias_method :to_hash, :values
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def finalize!
|
85
|
+
_settings.freeze
|
86
|
+
freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
# @api private
|
90
|
+
def pristine
|
91
|
+
self.class.new(_settings.pristine)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @api private
|
95
|
+
def respond_to_missing?(meth, include_private = false)
|
96
|
+
super || _settings.key?(resolve(meth))
|
158
97
|
end
|
159
98
|
|
160
99
|
private
|
161
100
|
|
162
|
-
# @private
|
163
|
-
def
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
self[setting.name] = setting.value
|
175
|
-
end
|
101
|
+
# @api private
|
102
|
+
def method_missing(meth, *args)
|
103
|
+
setting = _settings[resolve(meth)]
|
104
|
+
|
105
|
+
super unless setting
|
106
|
+
|
107
|
+
if setting.writer?(meth)
|
108
|
+
raise FrozenConfig, 'Cannot modify frozen config' if frozen?
|
109
|
+
|
110
|
+
_settings << setting.with(input: args[0])
|
111
|
+
else
|
112
|
+
setting.value
|
176
113
|
end
|
177
114
|
end
|
178
115
|
|
179
|
-
# @private
|
180
|
-
def
|
181
|
-
|
116
|
+
# @api private
|
117
|
+
def resolve(meth)
|
118
|
+
_resolved.fetch(meth) { _resolved[meth] = meth.to_s.tr('=', '').to_sym }
|
119
|
+
end
|
120
|
+
|
121
|
+
# @api private
|
122
|
+
def initialize_copy(source)
|
123
|
+
super
|
124
|
+
@_settings = source._settings.dup
|
182
125
|
end
|
183
126
|
end
|
184
127
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/configurable/constants'
|
4
|
+
require 'dry/configurable/setting'
|
5
|
+
require 'dry/configurable/settings'
|
6
|
+
require 'dry/configurable/compiler'
|
7
|
+
require 'dry/configurable/dsl/args'
|
8
|
+
|
9
|
+
module Dry
|
10
|
+
module Configurable
|
11
|
+
# Setting DSL used by the class API
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class DSL
|
15
|
+
VALID_NAME = /\A[a-z_]\w*\z/i.freeze
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
attr_reader :compiler
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
attr_reader :ast
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def initialize(&block)
|
25
|
+
@compiler = Compiler.new
|
26
|
+
@ast = []
|
27
|
+
instance_exec(&block) if block
|
28
|
+
end
|
29
|
+
|
30
|
+
# Register a new setting node and compile it into a setting object
|
31
|
+
#
|
32
|
+
# @see ClassMethods.setting
|
33
|
+
# @api public
|
34
|
+
# @return Setting
|
35
|
+
def setting(name, *args, &block)
|
36
|
+
unless VALID_NAME.match?(name.to_s)
|
37
|
+
raise ArgumentError, "#{name} is not a valid setting name"
|
38
|
+
end
|
39
|
+
|
40
|
+
args = Args.new(args)
|
41
|
+
|
42
|
+
args.ensure_valid_options
|
43
|
+
|
44
|
+
default, opts = args
|
45
|
+
|
46
|
+
node = [:setting, [name.to_sym, default, opts == default ? EMPTY_HASH : opts]]
|
47
|
+
|
48
|
+
if block
|
49
|
+
if block.arity.zero?
|
50
|
+
ast << [:nested, [node, DSL.new(&block).ast]]
|
51
|
+
else
|
52
|
+
ast << [:constructor, [node, block]]
|
53
|
+
end
|
54
|
+
else
|
55
|
+
ast << node
|
56
|
+
end
|
57
|
+
|
58
|
+
compiler.visit(ast.last)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|