dry-configurable 0.8.3 → 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.
@@ -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,94 +1,31 @@
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
- define_method(setting.name) do
26
- @config[setting.name]
27
- end
28
-
29
- define_method("#{setting.name}=") do |value|
30
- raise FrozenConfig, 'Cannot modify frozen config' if frozen?
31
- @config[setting.name] = setting.processor.(value)
32
- end
33
- end
3
+ require 'concurrent/map'
34
4
 
35
- @config_defined = true
36
- end
37
- end
38
-
39
- # @private
40
- def config_defined?
41
- @config_defined
42
- end
43
- end
44
-
45
- def initialize
46
- @config = ::Concurrent::Hash.new
47
- @lock = ::Mutex.new
48
- @defined = false
49
- end
5
+ require 'dry/equalizer'
50
6
 
51
- def defined?
52
- @defined
53
- end
7
+ require 'dry/configurable/constants'
8
+ require 'dry/configurable/errors'
54
9
 
55
- # @private
56
- def define!(parent_config = EMPTY_HASH)
57
- @lock.synchronize do
58
- break if self.defined?
59
-
60
- self.class.define_accessors!
61
- 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)
62
17
 
63
- @defined = true
64
- end
18
+ # @api private
19
+ attr_reader :_settings
65
20
 
66
- self
67
- end
21
+ # @api private
22
+ attr_reader :_resolved
68
23
 
69
- # @private
70
- def finalize!
71
- define!
72
- @config.freeze
73
- freeze
74
- end
75
-
76
- # Serialize config to a Hash
77
- #
78
- # @return [Hash]
79
- #
80
- # @api public
81
- def to_h
82
- @config.each_with_object({}) do |(key, value), hash|
83
- case value
84
- when Config
85
- hash[key] = value.to_h
86
- else
87
- hash[key] = value
88
- end
89
- end
24
+ # @api private
25
+ def initialize(settings)
26
+ @_settings = settings.dup
27
+ @_resolved = Concurrent::Map.new
90
28
  end
91
- alias to_hash to_h
92
29
 
93
30
  # Get config value by a key
94
31
  #
@@ -96,8 +33,10 @@ module Dry
96
33
  #
97
34
  # @return Config value
98
35
  def [](name)
99
- raise_unknown_setting_error(name) unless key?(name.to_sym)
100
- public_send(name)
36
+ name = name.to_sym
37
+ raise ArgumentError, "+#{name}+ is not a setting name" unless _settings.key?(name)
38
+
39
+ _settings[name].value
101
40
  end
102
41
 
103
42
  # Set config value.
@@ -106,63 +45,83 @@ module Dry
106
45
  # @param [String,Symbol] name
107
46
  # @param [Object] value
108
47
  def []=(name, value)
109
- raise_unknown_setting_error(name) unless key?(name.to_sym)
110
- public_send("#{name}=", value)
48
+ public_send(:"#{name}=", value)
111
49
  end
112
50
 
113
- # Whether config has a key
51
+ # Update config with new values
114
52
  #
115
- # @param [Symbol] key
116
- # @return [Bool]
117
- def key?(name)
118
- self.class.settings.name?(name)
119
- end
120
-
121
- # Recursively update values from a hash
53
+ # @param [Hash] A hash with new values
122
54
  #
123
- # @param [Hash] values to set
124
55
  # @return [Config]
56
+ #
57
+ # @api public
125
58
  def update(values)
126
59
  values.each do |key, value|
127
- if self[key].is_a?(Config)
60
+ case value
61
+ when Hash
128
62
  self[key].update(value)
129
63
  else
130
64
  self[key] = value
131
65
  end
132
66
  end
133
- self
134
67
  end
135
68
 
136
- def dup
137
- if self.defined?
138
- self.class.new.define!(to_h)
139
- else
140
- self.class.new
141
- 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))
142
97
  end
143
98
 
144
99
  private
145
100
 
146
- # @private
147
- def set_values!(parent)
148
- self.class.settings.each do |setting|
149
- if parent.key?(setting.name) && !setting.node?
150
- @config[setting.name] = parent[setting.name]
151
- elsif setting.undefined?
152
- @config[setting.name] = nil
153
- elsif setting.node?
154
- value = setting.value.create_config
155
- value.define!(parent.fetch(setting.name, EMPTY_HASH))
156
- self[setting.name] = value
157
- else
158
- self[setting.name] = setting.value
159
- 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
160
113
  end
161
114
  end
162
115
 
163
- # @private
164
- def raise_unknown_setting_error(name)
165
- 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
166
125
  end
167
126
  end
168
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