dry-configurable 0.8.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.
@@ -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,103 +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
- 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
24
+ # @api private
25
+ def initialize(settings)
26
+ @settings = settings.dup
27
+ @resolved = Concurrent::Map.new
74
28
  end
75
29
 
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
90
- end
91
- alias to_hash to_h
92
-
93
30
  # Get config value by a key
94
31
  #
95
32
  # @param [String,Symbol] name
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
+ raise ArgumentError, "+#{name}+ is not a setting name" unless settings.key?(name)
37
+
38
+ settings[name].value
101
39
  end
102
40
 
103
41
  # Set config value.
@@ -106,55 +44,83 @@ module Dry
106
44
  # @param [String,Symbol] name
107
45
  # @param [Object] value
108
46
  def []=(name, value)
109
- raise_unknown_setting_error(name) unless key?(name.to_sym)
110
- public_send("#{name}=", value)
47
+ public_send(:"#{name}=", value)
111
48
  end
112
49
 
113
- # Whether config has a key
50
+ # Update config with new values
114
51
  #
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
52
+ # @param [Hash] A hash with new values
122
53
  #
123
- # @param [Hash] values to set
124
54
  # @return [Config]
55
+ #
56
+ # @api public
125
57
  def update(values)
126
58
  values.each do |key, value|
127
- if self[key].is_a?(Config)
59
+ case value
60
+ when Hash
128
61
  self[key].update(value)
129
62
  else
130
63
  self[key] = value
131
64
  end
132
65
  end
133
- self
66
+ end
67
+
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))
134
96
  end
135
97
 
136
98
  private
137
99
 
138
- # @private
139
- def set_values!(parent)
140
- self.class.settings.each do |setting|
141
- if parent.key?(setting.name) && !setting.node?
142
- @config[setting.name] = parent[setting.name]
143
- elsif setting.undefined?
144
- @config[setting.name] = nil
145
- elsif setting.node?
146
- value = setting.value.create_config
147
- value.define!(parent.fetch(setting.name, EMPTY_HASH))
148
- self[setting.name] = value
149
- else
150
- self[setting.name] = setting.value
151
- 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
152
112
  end
153
113
  end
154
114
 
155
- # @private
156
- def raise_unknown_setting_error(name)
157
- 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
158
124
  end
159
125
  end
160
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
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/configurable/config'
4
+ require 'dry/configurable/methods'
5
+
6
+ module Dry
7
+ module Configurable
8
+ # Instance-level API when `Dry::Configurable` is included in a class
9
+ #
10
+ # @api public
11
+ module InstanceMethods
12
+ include Methods
13
+
14
+ # Return object's configuration
15
+ #
16
+ # @return [Config]
17
+ #
18
+ # @api public
19
+ attr_reader :config
20
+
21
+ # @api private
22
+ def initialize(*)
23
+ @config = Config.new(self.class._settings.dup)
24
+ super
25
+ end
26
+
27
+ # Finalize the config and freeze the object
28
+ #
29
+ # @api public
30
+ def finalize!
31
+ return self if frozen?
32
+
33
+ super
34
+ freeze
35
+ end
36
+
37
+ private
38
+
39
+ # @api public
40
+ def initialize_copy(source)
41
+ super
42
+ @config = source.config.dup
43
+ end
44
+ end
45
+ end
46
+ end