dry-configurable 0.8.0 → 0.11.0

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