dry-configurable 0.7.0 → 0.9.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 (40) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +10 -21
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/ci.yml +70 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +30 -0
  9. data/.gitignore +1 -1
  10. data/.rspec +3 -1
  11. data/.rubocop.yml +89 -0
  12. data/CHANGELOG.md +87 -6
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +4 -0
  16. data/LICENSE +1 -1
  17. data/README.md +20 -32
  18. data/docsite/source/index.html.md +55 -0
  19. data/docsite/source/testing.html.md +27 -0
  20. data/dry-configurable.gemspec +10 -4
  21. data/lib/dry/configurable.rb +124 -118
  22. data/lib/dry/configurable/config.rb +136 -43
  23. data/lib/dry/configurable/error.rb +0 -2
  24. data/lib/dry/configurable/setting.rb +46 -0
  25. data/lib/dry/configurable/settings.rb +117 -0
  26. data/lib/dry/configurable/settings/argument_parser.rb +50 -0
  27. data/lib/dry/configurable/test_interface.rb +5 -1
  28. data/lib/dry/configurable/version.rb +1 -1
  29. metadata +56 -38
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -34
  32. data/lib/dry/configurable/argument_parser.rb +0 -91
  33. data/lib/dry/configurable/config/value.rb +0 -27
  34. data/lib/dry/configurable/nested_config.rb +0 -33
  35. data/spec/integration/configurable_spec.rb +0 -25
  36. data/spec/spec_helper.rb +0 -92
  37. data/spec/support/shared_examples/configurable.rb +0 -391
  38. data/spec/unit/dry/configurable/argument_parser_spec.rb +0 -114
  39. data/spec/unit/dry/configurable/config/value_spec.rb +0 -55
  40. 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
- DEFAULT_PROCESSOR = ->(v) { v }.freeze
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
- def self.create(settings)
8
- klass = ::Class.new(self)
19
+ # @private
20
+ def define_accessors!
21
+ @lock.synchronize do
22
+ break if config_defined?
9
23
 
10
- settings.each do |setting|
11
- klass.__send__(:define_method, setting.name) do
12
- @config[setting.name]
13
- end
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
- klass.__send__(:define_method, "#{setting.name}=") do |value|
16
- raise_frozen_config if frozen?
17
- @config[setting.name] = setting.processor.call(value)
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
- klass.new(settings)
41
+ # @private
42
+ def config_defined?
43
+ @config_defined
44
+ end
22
45
  end
23
46
 
24
- def initialize(settings)
47
+ def initialize
25
48
  @config = ::Concurrent::Hash.new
26
-
27
- settings.each do |setting|
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 dup
37
- dup = super
38
- dup.instance_variable_set(:@config, @config.dup)
39
- dup
53
+ def defined?
54
+ @defined
40
55
  end
41
56
 
42
- def clone
43
- clone = super
44
- clone.instance_variable_set(:@config, @config.clone)
45
- clone
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 |tuple, hash|
55
- key, value = tuple
56
-
84
+ @config.each_with_object({}) do |(key, value), hash|
57
85
  case value
58
- when ::Dry::Configurable::Config, ::Dry::Configurable::NestedConfig
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
- raise_unknown_setting_error(name) unless setting?(name)
69
- public_send(name)
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
- raise_unknown_setting_error(name) unless setting?(name)
74
- public_send("#{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
75
127
  end
76
128
 
77
- private
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
- def raise_frozen_config
80
- raise FrozenConfig, 'Cannot modify frozen config'
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 raise_unknown_setting_error(name)
84
- raise ArgumentError, "+#{name}+ is not a setting name"
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
- def setting?(name)
88
- @config.key?(name.to_sym)
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
@@ -1,5 +1,3 @@
1
- # A collection of micro-libraries, each intended to encapsulate
2
- # a common task in Ruby
3
1
  module Dry
4
2
  module Configurable
5
3
  Error = Class.new(::StandardError)
@@ -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
@@ -8,7 +8,11 @@ module Dry
8
8
  #
9
9
  # @api public
10
10
  def reset_config
11
- create_config
11
+ @config = if self.is_a?(Module)
12
+ _settings.create_config
13
+ else
14
+ self.class._settings.create_config
15
+ end
12
16
  end
13
17
  end
14
18