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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -21
  3. data/LICENSE +1 -1
  4. data/README.md +15 -27
  5. data/dry-configurable.gemspec +27 -18
  6. data/lib/dry-configurable.rb +2 -0
  7. data/lib/dry/configurable.rb +21 -146
  8. data/lib/dry/configurable/class_methods.rb +103 -0
  9. data/lib/dry/configurable/compiler.rb +45 -0
  10. data/lib/dry/configurable/config.rb +79 -136
  11. data/lib/dry/configurable/constants.rb +12 -0
  12. data/lib/dry/configurable/dsl.rb +62 -0
  13. data/lib/dry/configurable/dsl/args.rb +58 -0
  14. data/lib/dry/configurable/{error.rb → errors.rb} +5 -1
  15. data/lib/dry/configurable/instance_methods.rb +46 -0
  16. data/lib/dry/configurable/methods.rb +32 -0
  17. data/lib/dry/configurable/setting.rb +91 -17
  18. data/lib/dry/configurable/settings.rb +42 -87
  19. data/lib/dry/configurable/test_interface.rb +3 -5
  20. data/lib/dry/configurable/version.rb +3 -1
  21. metadata +30 -25
  22. data/.codeclimate.yml +0 -12
  23. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  24. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  25. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  26. data/.github/workflows/ci.yml +0 -70
  27. data/.github/workflows/docsite.yml +0 -34
  28. data/.github/workflows/sync_configs.yml +0 -30
  29. data/.gitignore +0 -9
  30. data/.rspec +0 -4
  31. data/.rubocop.yml +0 -89
  32. data/CODE_OF_CONDUCT.md +0 -13
  33. data/CONTRIBUTING.md +0 -29
  34. data/Gemfile +0 -20
  35. data/Rakefile +0 -12
  36. data/docsite/source/index.html.md +0 -55
  37. data/docsite/source/testing.html.md +0 -27
  38. data/lib/dry/configurable/settings/argument_parser.rb +0 -50
  39. 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
- require 'concurrent/hash'
1
+ # frozen_string_literal: true
2
2
 
3
- module Dry
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
- def defined?
54
- @defined
55
- end
5
+ require 'dry/equalizer'
56
6
 
57
- # @private
58
- def define!(parent_config = EMPTY_HASH)
59
- @lock.synchronize do
60
- break if self.defined?
7
+ require 'dry/configurable/constants'
8
+ require 'dry/configurable/errors'
61
9
 
62
- self.class.define_accessors!
63
- set_values!(parent_config)
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
- @defined = true
66
- end
18
+ # @api private
19
+ attr_reader :_settings
67
20
 
68
- self
69
- end
21
+ # @api private
22
+ attr_reader :_resolved
70
23
 
71
- # @private
72
- def finalize!
73
- define!
74
- @config.freeze
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
- setting = self.class.settings[name.to_sym]
36
+ name = name.to_sym
37
+ raise ArgumentError, "+#{name}+ is not a setting name" unless _settings.key?(name)
102
38
 
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
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
- 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
48
+ public_send(:"#{name}=", value)
127
49
  end
128
50
 
129
- # Whether config has a key
51
+ # Update config with new values
130
52
  #
131
- # @param [Symbol] key
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
- if self[key].is_a?(Config)
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
- def dup
153
- if self.defined?
154
- self.class.new.define!(to_h)
155
- else
156
- self.class.new
157
- end
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 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
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 raise_unknown_setting_error(name)
181
- ::Kernel.raise ArgumentError, "+#{name}+ is not a setting name"
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/constants'
4
+
5
+ module Dry
6
+ # Shared constants
7
+ #
8
+ # @api private
9
+ module Configurable
10
+ include Dry::Core::Constants
11
+ end
12
+ 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