dry-configurable 0.15.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b464e3a14e23a75467fc56883a5e222ad96139fee156972e58729607113cba55
4
- data.tar.gz: 599d39ad241c3a6c17d27dd3ea8be2d94c1821a1c23ca9dea675126ba6c05ca6
3
+ metadata.gz: 0d8c3e4ebee8cf7f978c48580f64c1547c6c818b50ed7064b0758b4bffb072ee
4
+ data.tar.gz: 2aa701383b59289fbe591100642027fc5bc0d074bd0ec5d6d085359293af0f8f
5
5
  SHA512:
6
- metadata.gz: 8d306539d687ef4dc2ee1a999008ab3ead0f466cef01cc17b0122bc088d503da4515b906fa1d26449a4c1dd57b82d39cfd2558c4734061025fb6954383364ee3
7
- data.tar.gz: 421f2b5bc4a89b155d889ee4305183a3f97c42ab74db8cd9c41fecbccbb0f11ff739a4a01afa780da4291bf3404e6ed87c224093ee08f8de706ce46d30b39da8
6
+ metadata.gz: 221e2cfb38c4eae38ab6b56250e2d70672c9d1e6883b048f5a6bc807d0996065bf1b8e4a102fd78b6a95a4a4ffcf21dcfd46c64737acd915b3d1b80f9dca273e
7
+ data.tar.gz: 1f04a6b9af22d12a6b310902cdf5761362762a7fcca97c78a2feb17a28ce3199285a8cd1b6e80c56cde10f7fc2983c8598990fa263e5e55656502ad22761dd2e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 0.16.1 2022-10-13
4
+
5
+ ### Changed
6
+
7
+ - Restored performance of config value reads (direct reader methods as well as aggregate methods like `#values` and `#to_h`) to pre-0.16.0 levels (#149 by @timriley)
8
+
9
+ [Compare v0.16.0...v0.16.1](https://github.com/dry-rb/dry-configurable/compare/v0.16.0...v0.16.1)
10
+
11
+ ## 0.16.0 2022-10-08
12
+
13
+
14
+ ### Added
15
+
16
+ - Support for custom config classes via `config_class:` option (#136 by @solnic)
17
+
18
+ ```ruby
19
+ extend Dry::Configurable(config_class: MyConfig)
20
+ ```
21
+
22
+ Your config class should inherit from `Dry::Configurable::Config`.
23
+ - Return `Dry::Core::Constants::Undefined` (instead of nil) as the value for non-configured settings via a `default_undefined: true` option (#141 by @timriley)
24
+
25
+ ```ruby
26
+ extend Dry::Configurable(default_undefined: true)
27
+ ```
28
+
29
+ You must opt into this feature via the `default_undefined: true` option. Non-configured setting values are still `nil` by default.
30
+
31
+ ### Fixed
32
+
33
+ - Remove exec bit from version.rb (#139 by @Fryguy)
34
+
35
+ ### Changed
36
+
37
+ - Improve memory usage by separating setting definitions from config values (#138 by @timriley)
38
+
39
+ Your usage of dry-configurable may be impacted if you have been accessing objects from `_settings` or the internals of `Dry::Configurable::Config`. `_settings` now returns `Dry::Configurable::Setting` instances, which contain only the details from the setting's definition. Setting _values_ remain in `Dry::Configurable::Config`.
40
+ - Use Zeitwerk to speed up load time (#135 by @solnic)
41
+
42
+ [Compare v0.15.0...v0.16.0](https://github.com/dry-rb/dry-configurable/compare/v0.15.0...v0.16.0)
43
+
3
44
  ## 0.15.0 2022-04-21
4
45
 
5
46
 
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2021 dry-rb team
3
+ Copyright (c) 2015-2022 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
@@ -29,8 +29,8 @@ Gem::Specification.new do |spec|
29
29
  spec.required_ruby_version = ">= 2.7.0"
30
30
 
31
31
  # to update dependencies edit project.yml
32
- spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
33
32
  spec.add_runtime_dependency "dry-core", "~> 0.6"
33
+ spec.add_runtime_dependency "zeitwerk", "~> 2.6"
34
34
 
35
35
  spec.add_development_dependency "bundler"
36
36
  spec.add_development_dependency "rake"
@@ -2,11 +2,6 @@
2
2
 
3
3
  require "set"
4
4
 
5
- require "dry/configurable/constants"
6
- require "dry/configurable/dsl"
7
- require "dry/configurable/methods"
8
- require "dry/configurable/settings"
9
-
10
5
  module Dry
11
6
  module Configurable
12
7
  module ClassMethods
@@ -16,8 +11,17 @@ module Dry
16
11
  def inherited(subclass)
17
12
  super
18
13
 
19
- subclass.instance_variable_set("@_settings", _settings.dup)
20
- subclass.instance_variable_set("@_config", config.dup) if respond_to?(:config)
14
+ subclass.instance_variable_set(:@__config_extension__, __config_extension__)
15
+
16
+ new_settings = _settings.dup
17
+ subclass.instance_variable_set(:@_settings, new_settings)
18
+
19
+ # Only classes **extending** Dry::Configurable have class-level config. When
20
+ # Dry::Configurable is **included**, the class-level config method is undefined because it
21
+ # resides at the instance-level instead (see `Configurable.included`).
22
+ if respond_to?(:config)
23
+ subclass.instance_variable_set(:@config, config.dup_for_settings(new_settings))
24
+ end
21
25
  end
22
26
 
23
27
  # Add a setting to the configuration
@@ -52,7 +56,7 @@ module Dry
52
56
  #
53
57
  # @api public
54
58
  def settings
55
- @settings ||= Set[*_settings.map(&:name)]
59
+ Set[*_settings.map(&:name)]
56
60
  end
57
61
 
58
62
  # Return declared settings
@@ -70,17 +74,25 @@ module Dry
70
74
  #
71
75
  # @api public
72
76
  def config
73
- # The _settings provided to the Config remain shared between the class and the
74
- # Config. This allows settings defined _after_ accessing the config to become
75
- # available in subsequent accesses to the config. The config is duped when
76
- # subclassing to ensure it remains distinct between subclasses and parent classes
77
- # (see `.inherited` above).
78
- @config ||= Config.new(_settings)
77
+ @config ||= __config_build__
78
+ end
79
+
80
+ # @api private
81
+ def __config_build__(settings = _settings)
82
+ __config_extension__.config_class.new(settings)
83
+ end
84
+
85
+ # @api private
86
+ def __config_extension__
87
+ @__config_extension__
79
88
  end
80
89
 
81
90
  # @api private
82
91
  def __config_dsl__
83
- @__config_dsl__ ||= DSL.new
92
+ @__config_dsl__ ||= DSL.new(
93
+ config_class: __config_extension__.config_class,
94
+ default_undefined: __config_extension__.default_undefined
95
+ )
84
96
  end
85
97
 
86
98
  # @api private
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable/setting"
4
- require "dry/configurable/settings"
5
-
6
3
  module Dry
7
4
  module Configurable
8
5
  # Setting compiler used internally by the DSL
@@ -32,7 +29,9 @@ module Dry
32
29
  # @api private
33
30
  def visit_nested(node)
34
31
  parent, children = node
35
- visit(parent).nested(call(children))
32
+ name, opts = parent[1]
33
+
34
+ Setting.new(name, **opts, children: Settings.new(call(children)))
36
35
  end
37
36
  end
38
37
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
3
+ require "dry/core/constants"
4
+ require "set"
4
5
 
5
6
  require "dry/core/equalizer"
6
7
 
7
- require "dry/configurable/constants"
8
- require "dry/configurable/errors"
9
-
10
8
  module Dry
11
9
  module Configurable
12
10
  # Config exposes setting values through a convenient API
@@ -19,12 +17,29 @@ module Dry
19
17
  attr_reader :_settings
20
18
 
21
19
  # @api private
22
- attr_reader :_resolved
20
+ attr_reader :_values
21
+
22
+ # @api private
23
+ attr_reader :_configured
24
+ protected :_configured
23
25
 
24
26
  # @api private
25
- def initialize(settings)
27
+ def initialize(settings, values: {})
26
28
  @_settings = settings
27
- @_resolved = Concurrent::Map.new
29
+ @_values = values
30
+ @_configured = Set.new
31
+ end
32
+
33
+ # @api private
34
+ private def initialize_copy(source)
35
+ super
36
+ @_values = source.__send__(:dup_values)
37
+ @_configured = source._configured.dup
38
+ end
39
+
40
+ # @api private
41
+ def dup_for_settings(settings)
42
+ dup.tap { |config| config.instance_variable_set(:@_settings, settings) }
28
43
  end
29
44
 
30
45
  # Get config value by a key
@@ -34,9 +49,19 @@ module Dry
34
49
  # @return Config value
35
50
  def [](name)
36
51
  name = name.to_sym
37
- raise ArgumentError, "+#{name}+ is not a setting name" unless _settings.key?(name)
38
52
 
39
- _settings[name].value
53
+ unless (setting = _settings[name])
54
+ raise ArgumentError, "+#{name}+ is not a setting name"
55
+ end
56
+
57
+ _values.fetch(name) {
58
+ # Mutable settings may be configured after read
59
+ _configured.add(name) if setting.cloneable?
60
+
61
+ setting.to_value.tap { |value|
62
+ _values[name] = value
63
+ }
64
+ }
40
65
  end
41
66
 
42
67
  # Set config value.
@@ -45,7 +70,17 @@ module Dry
45
70
  # @param [String,Symbol] name
46
71
  # @param [Object] value
47
72
  def []=(name, value)
48
- public_send(:"#{name}=", value)
73
+ raise FrozenConfig, "Cannot modify frozen config" if frozen?
74
+
75
+ name = name.to_sym
76
+
77
+ unless (setting = _settings[name])
78
+ raise ArgumentError, "+#{name}+ is not a setting name"
79
+ end
80
+
81
+ _configured.add(name)
82
+
83
+ _values[name] = setting.constructor.(value)
49
84
  end
50
85
 
51
86
  # Update config with new values
@@ -70,61 +105,94 @@ module Dry
70
105
  self
71
106
  end
72
107
 
73
- # Dump config into a hash
108
+ # Returns true if the value for the given key has been set on this config.
109
+ #
110
+ # For simple values, this returns true if the value has been explicitly assigned.
111
+ #
112
+ # For cloneable (mutable) values, since these are captured on read, returns true if the value
113
+ # does not compare equally to its corresdponing default value. This relies on these objects
114
+ # having functioning `#==` checks.
115
+ #
116
+ # @return [Bool]
117
+ #
118
+ # @api public
119
+ def configured?(key)
120
+ if _configured.include?(key) && _settings[key].cloneable?
121
+ return _values[key] != _settings[key].to_value
122
+ end
123
+
124
+ _configured.include?(key)
125
+ end
126
+
127
+ # Returns the current config values.
128
+ #
129
+ # Nested configs remain in their {Config} instances.
74
130
  #
75
131
  # @return [Hash]
76
132
  #
77
133
  # @api public
78
134
  def values
79
- _settings
80
- .map { |setting| [setting.name, setting.value] }
81
- .map { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] }
82
- .to_h
135
+ # Ensure all settings are represented in values
136
+ _settings.each { |setting| self[setting.name] unless _values.key?(setting.name) }
137
+
138
+ _values
139
+ end
140
+
141
+ # Returns config values as a hash, with nested values also converted from {Config} instances
142
+ # into hashes.
143
+ #
144
+ # @return [Hash]
145
+ #
146
+ # @api public
147
+ def to_h
148
+ values.to_h { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] }
83
149
  end
84
- alias_method :to_h, :values
85
150
 
86
151
  # @api private
87
152
  def finalize!(freeze_values: false)
88
- _settings.finalize!(freeze_values: freeze_values)
153
+ values.each_value do |value|
154
+ if value.is_a?(self.class)
155
+ value.finalize!(freeze_values: freeze_values)
156
+ elsif freeze_values
157
+ value.freeze
158
+ end
159
+ end
160
+
89
161
  freeze
90
162
  end
91
163
 
92
164
  # @api private
93
165
  def pristine
94
- self.class.new(_settings.pristine)
95
- end
96
-
97
- # @api private
98
- def respond_to_missing?(meth, include_private = false)
99
- super || _settings.key?(resolve(meth))
166
+ self.class.new(_settings)
100
167
  end
101
168
 
102
169
  private
103
170
 
104
- # @api private
105
- def method_missing(meth, *args)
106
- setting = _settings[resolve(meth)]
171
+ def method_missing(name, *args)
172
+ setting_name = setting_name_from_method(name)
173
+ setting = _settings[setting_name]
107
174
 
108
175
  super unless setting
109
176
 
110
- if setting.writer?(meth)
111
- raise FrozenConfig, "Cannot modify frozen config" if frozen?
112
-
113
- _settings << setting.with(input: args[0])
177
+ if name.end_with?("=")
178
+ self[setting_name] = args[0]
114
179
  else
115
- setting.value
180
+ self[setting_name]
116
181
  end
117
182
  end
118
183
 
119
- # @api private
120
- def resolve(meth)
121
- _resolved.fetch(meth) { _resolved[meth] = meth.to_s.tr("=", "").to_sym }
184
+ def respond_to_missing?(meth, include_private = false)
185
+ _settings.key?(setting_name_from_method(meth)) || super
122
186
  end
123
187
 
124
- # @api private
125
- def initialize_copy(source)
126
- super
127
- @_settings = source._settings.dup
188
+ def setting_name_from_method(method_name)
189
+ method_name.to_s.tr("=", "").to_sym
190
+ end
191
+
192
+ def dup_values
193
+ _values.each_with_object({}) { |(key, val), dup_hsh|
194
+ dup_hsh[key] = _settings[key].cloneable? ? val.dup : val
195
+ }
128
196
  end
129
197
  end
130
198
  end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable/constants"
4
- require "dry/configurable/flags"
5
- require "dry/configurable/setting"
6
- require "dry/configurable/settings"
7
- require "dry/configurable/compiler"
8
3
  require "dry/core/deprecations"
9
4
 
10
5
  module Dry
@@ -15,16 +10,16 @@ module Dry
15
10
  class DSL
16
11
  VALID_NAME = /\A[a-z_]\w*\z/i.freeze
17
12
 
18
- # @api private
19
13
  attr_reader :compiler
20
14
 
21
- # @api private
22
15
  attr_reader :ast
23
16
 
24
- # @api private
25
- def initialize(&block)
17
+ attr_reader :options
18
+
19
+ def initialize(**options, &block)
26
20
  @compiler = Compiler.new
27
21
  @ast = []
22
+ @options = options
28
23
  instance_exec(&block) if block
29
24
  end
30
25
 
@@ -33,106 +28,15 @@ module Dry
33
28
  # @see ClassMethods.setting
34
29
  # @api private
35
30
  # @return Setting
36
- def setting(name, default = Undefined, **options, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
31
+ def setting(name, **options, &block) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
37
32
  unless VALID_NAME.match?(name.to_s)
38
33
  raise ArgumentError, "#{name} is not a valid setting name"
39
34
  end
40
35
 
41
- if default != Undefined
42
- if Dry::Configurable.warn_on_setting_positional_default
43
- Dry::Core::Deprecations.announce(
44
- "default value as positional argument to settings",
45
- "Provide a `default:` keyword argument instead",
46
- tag: "dry-configurable",
47
- uplevel: 2
48
- )
49
- end
50
-
51
- options = options.merge(default: default)
52
- end
53
-
54
- if RUBY_VERSION < "3.0" &&
55
- default == Undefined &&
56
- (valid_opts, invalid_opts = valid_and_invalid_options(options)) &&
57
- invalid_opts.any? &&
58
- valid_opts.none?
59
- # In Ruby 2.6 and 2.7, when a hash is given as the second positional argument
60
- # (i.e. the hash is intended to be the setting's default value), and there are
61
- # no other keyword arguments given, the hash is assigned to the `options`
62
- # variable instead of `default`.
63
- #
64
- # For example, for this setting:
65
- #
66
- # setting :hash_setting, {my_hash: true}
67
- #
68
- # We'll have a `default` of `Undefined` and an `options` of `{my_hash: true}`
69
- #
70
- # If any additional keyword arguments are provided, e.g.:
71
- #
72
- # setting :hash_setting, {my_hash: true}, reader: true
73
- #
74
- # Then we'll have a `default` of `{my_hash: true}` and an `options` of `{reader:
75
- # true}`, which is what we want.
76
- #
77
- # To work around that first case and ensure our (deprecated) backwards
78
- # compatibility holds for Ruby 2.6 and 2.7, we extract all invalid options from
79
- # `options`, and if there are no remaining valid options (i.e. if there were no
80
- # keyword arguments given), then we can infer the invalid options to be a
81
- # default hash value for the setting.
82
- #
83
- # This approach also preserves the behavior of raising an ArgumentError when a
84
- # distinct hash is _not_ intentionally provided as the second positional
85
- # argument (i.e. it's not enclosed in braces), and instead invalid keyword
86
- # arguments are given alongside valid ones. So this setting:
87
- #
88
- # setting :some_setting, invalid_option: true, reader: true
89
- #
90
- # Would raise an ArgumentError as expected.
91
- #
92
- # However, the one case we can't catch here is when invalid options are supplied
93
- # without hash literal braces, but there are no other keyword arguments
94
- # supplied. In this case, a setting like:
95
- #
96
- # setting :hash_setting, my_hash: true
97
- #
98
- # Is parsed identically to the first case described above:
99
- #
100
- # setting :hash_setting, {my_hash: true}
101
- #
102
- # So in both of these cases, the default value will become `{my_hash: true}`. We
103
- # consider this unlikely to be a problem in practice, since users are not likely
104
- # to be providing invalid options to `setting` and expecting them to be ignored.
105
- # Additionally, the deprecation messages will make the new behavior obvious, and
106
- # encourage the users to upgrade their setting definitions.
107
-
108
- if Dry::Configurable.warn_on_setting_positional_default
109
- Dry::Core::Deprecations.announce(
110
- "default value as positional argument to settings",
111
- "Provide a `default:` keyword argument instead",
112
- tag: "dry-configurable",
113
- uplevel: 2
114
- )
115
- end
116
-
117
- options = {default: invalid_opts}
118
- end
119
-
120
- if block && !block.arity.zero?
121
- if Dry::Configurable.warn_on_setting_constructor_block
122
- Dry::Core::Deprecations.announce(
123
- "passing a constructor as a block",
124
- "Provide a `constructor:` keyword argument instead",
125
- tag: "dry-configurable",
126
- uplevel: 2
127
- )
128
- end
129
-
130
- options = options.merge(constructor: block)
131
- block = nil
132
- end
133
-
134
36
  ensure_valid_options(options)
135
37
 
38
+ options = {default: default, config_class: config_class, **options}
39
+
136
40
  node = [:setting, [name.to_sym, options]]
137
41
 
138
42
  if block
@@ -144,6 +48,14 @@ module Dry
144
48
  compiler.visit(ast.last)
145
49
  end
146
50
 
51
+ def config_class
52
+ options[:config_class]
53
+ end
54
+
55
+ def default
56
+ options[:default_undefined] ? Undefined : nil
57
+ end
58
+
147
59
  private
148
60
 
149
61
  def ensure_valid_options(options)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Configurable
5
+ class Extension < Module
6
+ # @api private
7
+ attr_reader :config_class
8
+
9
+ # @api private
10
+ attr_reader :default_undefined
11
+
12
+ # @api private
13
+ def initialize(config_class: Configurable::Config, default_undefined: false)
14
+ super()
15
+ @config_class = config_class
16
+ @default_undefined = default_undefined
17
+ freeze
18
+ end
19
+
20
+ # @api private
21
+ def extended(klass)
22
+ super
23
+ klass.extend(ClassMethods)
24
+ klass.instance_variable_set(:@__config_extension__, self)
25
+ end
26
+
27
+ # @api private
28
+ def included(klass)
29
+ raise AlreadyIncluded if klass.include?(InstanceMethods)
30
+
31
+ super
32
+
33
+ klass.class_eval do
34
+ extend(ClassMethods)
35
+ include(InstanceMethods)
36
+ prepend(Initializer)
37
+
38
+ class << self
39
+ undef :config
40
+ undef :configure
41
+ end
42
+ end
43
+
44
+ klass.instance_variable_set(:@__config_extension__, self)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -12,10 +12,7 @@ module Dry
12
12
  module Initializer
13
13
  # @api private
14
14
  def initialize(*)
15
- # Dup settings at time of initializing to ensure setting values are specific to
16
- # this instance. This does mean that any settings defined on the class _after_
17
- # initialization will not be available on the instance.
18
- @config = Config.new(self.class._settings.dup)
15
+ @config = self.class.__config_build__(self.class._settings)
19
16
 
20
17
  super
21
18
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/configurable/errors"
4
-
5
3
  module Dry
6
4
  module Configurable
7
5
  # Common API for both classes and instances
@@ -4,18 +4,15 @@ require "set"
4
4
 
5
5
  require "dry/core/equalizer"
6
6
 
7
- require "dry/configurable/constants"
8
- require "dry/configurable/config"
9
-
10
7
  module Dry
11
8
  module Configurable
12
9
  # This class represents a setting and is used internally.
13
10
  #
14
11
  # @api private
15
12
  class Setting
16
- include Dry::Equalizer(:name, :value, :options, inspect: false)
13
+ include Dry::Equalizer(:name, :default, :constructor, :children, :options, inspect: false)
17
14
 
18
- OPTIONS = %i[input default reader constructor cloneable settings].freeze
15
+ OPTIONS = %i[default reader constructor cloneable settings config_class].freeze
19
16
 
20
17
  DEFAULT_CONSTRUCTOR = -> v { v }.freeze
21
18
 
@@ -25,33 +22,19 @@ module Dry
25
22
  attr_reader :name
26
23
 
27
24
  # @api private
28
- attr_reader :writer_name
25
+ attr_reader :default
29
26
 
30
27
  # @api private
31
- attr_reader :input
28
+ attr_reader :cloneable
32
29
 
33
30
  # @api private
34
- attr_reader :default
31
+ attr_reader :constructor
35
32
 
36
33
  # @api private
37
- attr_reader :options
34
+ attr_reader :children
38
35
 
39
- # Specialized Setting which includes nested settings
40
- #
41
36
  # @api private
42
- class Nested < Setting
43
- CONSTRUCTOR = Config.method(:new)
44
-
45
- # @api private
46
- def pristine
47
- with(input: input.pristine)
48
- end
49
-
50
- # @api private
51
- def constructor
52
- CONSTRUCTOR
53
- end
54
- end
37
+ attr_reader :options
55
38
 
56
39
  # @api private
57
40
  def self.cloneable_value?(value)
@@ -59,71 +42,21 @@ module Dry
59
42
  end
60
43
 
61
44
  # @api private
62
- def initialize(name, input: Undefined, default: Undefined, **options)
45
+ def initialize(
46
+ name,
47
+ default:,
48
+ constructor: DEFAULT_CONSTRUCTOR,
49
+ children: EMPTY_ARRAY,
50
+ **options
51
+ )
63
52
  @name = name
64
- @writer_name = :"#{name}="
65
- @options = options
66
-
67
- # Setting collections (see `Settings`) are shared between the configurable class
68
- # and its `config` object, so for cloneable individual settings, we duplicate
69
- # their _values_ as early as possible to ensure no impact from unintended mutation
70
- @input = input
71
53
  @default = default
72
- if cloneable?
73
- @input = input.dup
74
- @default = default.dup
75
- end
76
-
77
- evaluate if input_defined?
78
- end
79
-
80
- # @api private
81
- def input_defined?
82
- !input.equal?(Undefined)
83
- end
84
-
85
- # @api private
86
- def value
87
- return @value if evaluated?
88
-
89
- @value = constructor[Undefined.coalesce(input, default, nil)]
90
- end
91
- alias_method :evaluate, :value
92
- private :evaluate
93
-
94
- # @api private
95
- def evaluated?
96
- instance_variable_defined?(:@value)
97
- end
98
-
99
- # @api private
100
- def nested(settings)
101
- Nested.new(name, input: settings, **options)
102
- end
103
-
104
- # @api private
105
- def pristine
106
- with(input: Undefined)
107
- end
108
-
109
- # @api private
110
- def finalize!(freeze_values: false)
111
- if value.is_a?(Config)
112
- value.finalize!(freeze_values: freeze_values)
113
- elsif freeze_values
114
- value.freeze
115
- end
116
- freeze
117
- end
118
-
119
- # @api private
120
- def with(new_opts)
121
- self.class.new(name, input: input, default: default, **options, **new_opts)
122
- end
123
-
124
- # @api private
125
- def constructor
126
- options[:constructor] || DEFAULT_CONSTRUCTOR
54
+ @cloneable = children.any? || options.fetch(:cloneable) {
55
+ Setting.cloneable_value?(default)
56
+ }
57
+ @constructor = constructor
58
+ @children = children
59
+ @options = options
127
60
  end
128
61
 
129
62
  # @api private
@@ -131,36 +64,20 @@ module Dry
131
64
  options[:reader].equal?(true)
132
65
  end
133
66
 
134
- # @api private
135
- def writer?(meth)
136
- writer_name.equal?(meth)
137
- end
138
-
139
67
  # @api private
140
68
  def cloneable?
141
- if options.key?(:cloneable)
142
- # Return cloneable option if explicitly set
143
- options[:cloneable]
144
- else
145
- # Otherwise, infer cloneable from any of the input, default, or value
146
- Setting.cloneable_value?(input) || Setting.cloneable_value?(default) || (
147
- evaluated? && Setting.cloneable_value?(value)
148
- )
149
- end
69
+ cloneable
150
70
  end
151
71
 
152
- private
153
-
154
72
  # @api private
155
- def initialize_copy(source)
156
- super
157
-
158
- @options = source.options.dup
73
+ def to_value
74
+ if children.any?
75
+ (options[:config_class] || Config).new(children)
76
+ else
77
+ value = default
78
+ value = constructor.(value) unless value.eql?(Undefined)
159
79
 
160
- if source.cloneable?
161
- @input = source.input.dup
162
- @default = source.default.dup
163
- @value = source.value.dup if source.evaluated?
80
+ cloneable? ? value.dup : value
164
81
  end
165
82
  end
166
83
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
4
-
5
3
  require "dry/core/equalizer"
6
- require "dry/configurable/constants"
7
4
 
8
5
  module Dry
9
6
  module Configurable
@@ -11,27 +8,27 @@ module Dry
11
8
  #
12
9
  # @api private
13
10
  class Settings
14
- include Dry::Equalizer(:elements)
11
+ include Dry::Equalizer(:settings)
15
12
 
16
13
  include Enumerable
17
14
 
18
15
  # @api private
19
- attr_reader :elements
16
+ attr_reader :settings
20
17
 
21
18
  # @api private
22
- def initialize(elements = EMPTY_ARRAY)
23
- initialize_elements(elements)
19
+ def initialize(settings = EMPTY_ARRAY)
20
+ @settings = settings.each_with_object({}) { |s, m| m[s.name] = s }
24
21
  end
25
22
 
26
23
  # @api private
27
24
  def <<(setting)
28
- elements[setting.name] = setting
25
+ settings[setting.name] = setting
29
26
  self
30
27
  end
31
28
 
32
29
  # @api private
33
30
  def [](name)
34
- elements[name]
31
+ settings[name]
35
32
  end
36
33
 
37
34
  # @api private
@@ -41,37 +38,18 @@ module Dry
41
38
 
42
39
  # @api private
43
40
  def keys
44
- elements.keys
41
+ settings.keys
45
42
  end
46
43
 
47
44
  # @api private
48
45
  def each(&block)
49
- elements.values.each(&block)
50
- end
51
-
52
- # @api private
53
- def pristine
54
- self.class.new(map(&:pristine))
55
- end
56
-
57
- # @api private
58
- def finalize!(freeze_values: false)
59
- each { |element| element.finalize!(freeze_values: freeze_values) }
60
- freeze
46
+ settings.each_value(&block)
61
47
  end
62
48
 
63
49
  private
64
50
 
65
- # @api private
66
51
  def initialize_copy(source)
67
- initialize_elements(source.map(&:dup))
68
- end
69
-
70
- # @api private
71
- def initialize_elements(elements)
72
- @elements = elements.each_with_object(Concurrent::Map.new) { |s, m|
73
- m[s.name] = s
74
- }
52
+ @settings = source.settings.dup
75
53
  end
76
54
  end
77
55
  end
@@ -3,6 +3,6 @@
3
3
  module Dry
4
4
  module Configurable
5
5
  # @api public
6
- VERSION = "0.15.0"
6
+ VERSION = "0.16.1"
7
7
  end
8
8
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/array"
3
+ require "zeitwerk"
4
4
 
5
+ require "dry/core/constants"
5
6
  require "dry/configurable/constants"
6
- require "dry/configurable/class_methods"
7
- require "dry/configurable/instance_methods"
8
- require "dry/configurable/config"
9
- require "dry/configurable/setting"
10
7
  require "dry/configurable/errors"
11
8
 
12
9
  module Dry
10
+ # @api public
11
+ def self.Configurable(**options)
12
+ Configurable::Extension.new(**options)
13
+ end
14
+
13
15
  # A simple configuration mixin
14
16
  #
15
17
  # @example class-level configuration
@@ -44,27 +46,32 @@ module Dry
44
46
  #
45
47
  # @api public
46
48
  module Configurable
49
+ def self.loader
50
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
51
+ root = File.expand_path("..", __dir__)
52
+ loader.tag = "dry-configurable"
53
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dry-configurable.rb")
54
+ loader.push_dir(root)
55
+ loader.ignore(
56
+ "#{root}/dry-configurable.rb",
57
+ "#{root}/dry/configurable/{constants,errors,version}.rb"
58
+ )
59
+ loader.inflector.inflect("dsl" => "DSL")
60
+ end
61
+ end
62
+
47
63
  # @api private
48
64
  def self.extended(klass)
49
65
  super
50
- klass.extend(ClassMethods)
66
+ klass.extend(Extension.new)
51
67
  end
52
68
 
53
69
  # @api private
54
70
  def self.included(klass)
55
- raise AlreadyIncluded if klass.include?(InstanceMethods)
56
-
57
71
  super
58
- klass.class_eval do
59
- extend(ClassMethods)
60
- include(InstanceMethods)
61
- prepend(Initializer)
62
-
63
- class << self
64
- undef :config
65
- undef :configure
66
- end
67
- end
72
+ klass.include(Extension.new)
68
73
  end
74
+
75
+ loader.setup
69
76
  end
70
77
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-configurable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Holland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-21 00:00:00.000000000 Z
11
+ date: 2022-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
14
+ name: dry-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '0.6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: '0.6'
27
27
  - !ruby/object:Gem::Dependency
28
- name: dry-core
28
+ name: zeitwerk
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.6'
33
+ version: '2.6'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.6'
40
+ version: '2.6'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -99,7 +99,7 @@ files:
99
99
  - lib/dry/configurable/constants.rb
100
100
  - lib/dry/configurable/dsl.rb
101
101
  - lib/dry/configurable/errors.rb
102
- - lib/dry/configurable/flags.rb
102
+ - lib/dry/configurable/extension.rb
103
103
  - lib/dry/configurable/instance_methods.rb
104
104
  - lib/dry/configurable/methods.rb
105
105
  - lib/dry/configurable/setting.rb
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/core/class_attributes"
4
-
5
- module Dry
6
- module Configurable
7
- extend Core::ClassAttributes
8
-
9
- # Set to false to suppress deprecation warning when a setting default is provided as a
10
- # positional argument
11
- defines :warn_on_setting_positional_default
12
- warn_on_setting_positional_default true
13
-
14
- # Set to false to suppress deprecation warning when a setting constructor is provided
15
- # as a block
16
- defines :warn_on_setting_constructor_block
17
- warn_on_setting_constructor_block true
18
- end
19
- end