dry-configurable 0.7.0 → 0.9.0

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