dry-configurable 0.9.0 → 0.11.3

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