dry-configurable 0.9.0 → 0.11.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -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 +78 -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 -18
  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,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,41 @@
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
+ raise ArgumentError, "+#{name}+ is not a setting name" unless settings.key?(name)
102
37
 
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
38
+ settings[name].value
110
39
  end
111
40
 
112
41
  # Set config value.
@@ -115,70 +44,83 @@ module Dry
115
44
  # @param [String,Symbol] name
116
45
  # @param [Object] value
117
46
  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
47
+ public_send(:"#{name}=", value)
127
48
  end
128
49
 
129
- # Whether config has a key
50
+ # Update config with new values
130
51
  #
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
52
+ # @param [Hash] A hash with new values
138
53
  #
139
- # @param [Hash] values to set
140
54
  # @return [Config]
55
+ #
56
+ # @api public
141
57
  def update(values)
142
58
  values.each do |key, value|
143
- if self[key].is_a?(Config)
59
+ case value
60
+ when Hash
144
61
  self[key].update(value)
145
62
  else
146
63
  self[key] = value
147
64
  end
148
65
  end
149
- self
150
66
  end
151
67
 
152
- def dup
153
- if self.defined?
154
- self.class.new.define!(to_h)
155
- else
156
- self.class.new
157
- end
68
+ # Dump config into a hash
69
+ #
70
+ # @return [Hash]
71
+ #
72
+ # @api public
73
+ def values
74
+ settings
75
+ .map { |setting| [setting.name, setting.value] }
76
+ .map { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] }
77
+ .to_h
78
+ end
79
+ alias_method :to_h, :values
80
+ alias_method :to_hash, :values
81
+
82
+ # @api private
83
+ def finalize!
84
+ settings.freeze
85
+ freeze
86
+ end
87
+
88
+ # @api private
89
+ def pristine
90
+ self.class.new(settings.pristine)
91
+ end
92
+
93
+ # @api private
94
+ def respond_to_missing?(meth, include_private = false)
95
+ super || settings.key?(resolve(meth))
158
96
  end
159
97
 
160
98
  private
161
99
 
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
100
+ # @api private
101
+ def method_missing(meth, *args)
102
+ setting = settings[resolve(meth)]
103
+
104
+ super unless setting
105
+
106
+ if setting.writer?(meth)
107
+ raise FrozenConfig, 'Cannot modify frozen config' if frozen?
108
+
109
+ settings << setting.with(input: args[0])
110
+ else
111
+ setting.value
176
112
  end
177
113
  end
178
114
 
179
- # @private
180
- def raise_unknown_setting_error(name)
181
- ::Kernel.raise ArgumentError, "+#{name}+ is not a setting name"
115
+ # @api private
116
+ def resolve(meth)
117
+ resolved.fetch(meth) { resolved[meth] = meth.to_s.tr('=', '').to_sym }
118
+ end
119
+
120
+ # @api private
121
+ def initialize_copy(source)
122
+ super
123
+ @settings = source.settings.dup
182
124
  end
183
125
  end
184
126
  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, 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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/configurable/constants'
4
+ require 'dry/configurable/setting'
5
+
6
+ module Dry
7
+ module Configurable
8
+ class DSL
9
+ # @api private
10
+ class Args
11
+ # @api private
12
+ attr_reader :args
13
+
14
+ # @api private
15
+ attr_reader :size
16
+
17
+ # @api private
18
+ attr_reader :opts
19
+
20
+ # @api private
21
+ def initialize(args)
22
+ @args = args
23
+ @size = args.size
24
+ @opts = Setting::OPTIONS
25
+ end
26
+
27
+ # @api private
28
+ def ensure_valid_options
29
+ return unless options
30
+
31
+ keys = options.keys - opts
32
+ raise ArgumentError, "Invalid options: #{keys.inspect}" unless keys.empty?
33
+ end
34
+
35
+ # @api private
36
+ def to_ary
37
+ [default, options || EMPTY_HASH]
38
+ end
39
+
40
+ # @api private
41
+ def default
42
+ if size.equal?(1) && options.nil?
43
+ args[0]
44
+ elsif size > 1 && options
45
+ args[0]
46
+ else
47
+ Undefined
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ def options
53
+ args.detect { |arg| arg.is_a?(Hash) && (opts & arg.keys).any? }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
4
+ # Shared errors
5
+ #
6
+ # @api public
2
7
  module Configurable
3
8
  Error = Class.new(::StandardError)
4
- AlreadyDefinedConfig = ::Class.new(Error)
5
9
  FrozenConfig = ::Class.new(Error)
6
10
  end
7
11
  end