dry-configurable 0.12.1 → 0.16.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38b75c91290021054add38bdd9e3313f17aa133e219330a9b02a83227abd101c
4
- data.tar.gz: 4b808bd3880c35e2e80a702e4577f2b2da35ef2b31667f4314accc3cf0c20470
3
+ metadata.gz: 0d8c3e4ebee8cf7f978c48580f64c1547c6c818b50ed7064b0758b4bffb072ee
4
+ data.tar.gz: 2aa701383b59289fbe591100642027fc5bc0d074bd0ec5d6d085359293af0f8f
5
5
  SHA512:
6
- metadata.gz: efdd98ed5f60acf8090296954942f01c21702c29fa60c1edea21e0fbbb185955963b268191cb042ee33cc4651f4350419e7cbc2b7125c36103dce2733d137fa4
7
- data.tar.gz: 21d278a4f9f4a90d530145408585c30e004ce2830d5777a408bd9ea83bdfd5cba2ce58f1a1ca46f1212d0e8d5967891a25467f39e59a8af20291cf5ca1beff3f
6
+ metadata.gz: 221e2cfb38c4eae38ab6b56250e2d70672c9d1e6883b048f5a6bc807d0996065bf1b8e4a102fd78b6a95a4a4ffcf21dcfd46c64737acd915b3d1b80f9dca273e
7
+ data.tar.gz: 1f04a6b9af22d12a6b310902cdf5761362762a7fcca97c78a2feb17a28ce3199285a8cd1b6e80c56cde10f7fc2983c8598990fa263e5e55656502ad22761dd2e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,123 @@
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
+
44
+ ## 0.15.0 2022-04-21
45
+
46
+
47
+ ### Changed
48
+
49
+ - The `finalize!` method (as class or instance method, depending on whether you extend or include `Dry::Configurable` respectively) now accepts a boolean `freeze_values:` argument, which if true, will recursively freeze all config values in addition to the `config` itself. (#105 by @ojab)
50
+
51
+ ```ruby
52
+ class MyConfigurable
53
+ include Dry::Configurable
54
+
55
+ setting :db, default: "postgre"
56
+ end
57
+
58
+ my_obj = MyConfigurable.new
59
+ my_obj.finalize!(freeze_values: true)
60
+ my_obj.config.db << "sql" # Will raise FrozenError
61
+ ```
62
+ - `Dry::Configurable::Config#update` will set hashes as values for non-nested settings (#131 by @ojab)
63
+
64
+ ```ruby
65
+ class MyConfigurable
66
+ extend Dry::Configurable
67
+
68
+ setting :sslcert, constructor: ->(v) { v&.values_at(:pem, :pass)&.join }
69
+ end
70
+
71
+ MyConfigurable.config.update(sslcert: {pem: "cert", pass: "qwerty"})
72
+ MyConfigurable.config.sslcert # => "certqwerty"
73
+ ```
74
+ - `Dry::Configurable::Config#update` will accept any values implicitly convertible to hash via `#to_hash` (#133 by @timriley)
75
+
76
+ [Compare v0.14.0...v0.15.0](https://github.com/dry-rb/dry-configurable/compare/v0.14.0...v0.15.0)
77
+
78
+ ## 0.14.0 2022-01-14
79
+
80
+
81
+ ### Changed
82
+
83
+ - Settings defined after an access to `config` will still be made available on that `config`. (#130 by @timriley)
84
+ - Cloneable settings are cloned immediately upon assignment. (#130 by @timriley)
85
+ - Changes to config values in parent classes after subclasses have already been created will not be propogated to those subclasses. Subclasses created _after_ config values have been changed in the parent _will_ receive those config values. (#130 by @timriley)
86
+
87
+ [Compare v0.13.0...v0.14.0](https://github.com/dry-rb/dry-configurable/compare/v0.13.0...v0.14.0)
88
+
89
+ ## 0.13.0 2021-09-12
90
+
91
+
92
+ ### Added
93
+
94
+ - Added flags to determine whether to warn on the API usage deprecated in this release (see "Changed" section below). Set these to `false` to suppress the warnings. (#124 by @timriley)
95
+
96
+ ```ruby
97
+ Dry::Configurable.warn_on_setting_constructor_block false
98
+ Dry::Configurable.warn_on_setting_positional_default false
99
+ ```
100
+
101
+ ### Fixed
102
+
103
+ - Fixed `ArgumentError` for classes including `Dry::Configurable` whose `initializer` has required kwargs. (#113 by @timriley)
104
+
105
+ ### Changed
106
+
107
+ - Deprecated the setting constructor provided as a block. Provide it via the `constructor:` keyword argument instead. (#111 by @waiting-for-dev & @timriley)
108
+
109
+ ```ruby
110
+ setting :path, constructor: -> path { Pathname(path) }
111
+ ```
112
+ - Deprecated the setting default provided as the second positional argument. Provide it via the `default:` keyword argument instead. (#112 and #121 by @waiting-for-dev & @timriley)
113
+
114
+ ```ruby
115
+ setting :path, default: "some/default/path"
116
+ ```
117
+ - [BREAKING] Removed implicit `to_hash` conversion from `Config`. (#114 by @timriley)
118
+
119
+ [Compare v0.12.1...v0.13.0](https://github.com/dry-rb/dry-configurable/compare/v0.12.1...v0.13.0)
120
+
3
121
  ## 0.12.1 2021-02-15
4
122
 
5
123
 
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
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!--- this file is synced from dry-rb/template-gem project -->
1
2
  [gem]: https://rubygems.org/gems/dry-configurable
2
3
  [actions]: https://github.com/dry-rb/dry-configurable/actions
3
4
  [codacy]: https://www.codacy.com/gh/dry-rb/dry-configurable
@@ -10,19 +11,19 @@
10
11
  [![CI Status](https://github.com/dry-rb/dry-configurable/workflows/ci/badge.svg)][actions]
11
12
  [![Codacy Badge](https://api.codacy.com/project/badge/Grade/0276a97990e04eb0ac722b3e7f3620b5)][codacy]
12
13
  [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/0276a97990e04eb0ac722b3e7f3620b5)][codacy]
13
- [![Inline docs](http://inch-ci.org/github/dry-rb/dry-configurable.svg?branch=master)][inchpages]
14
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-configurable.svg?branch=main)][inchpages]
14
15
 
15
16
  ## Links
16
17
 
17
- * [User documentation](http://dry-rb.org/gems/dry-configurable)
18
+ * [User documentation](https://dry-rb.org/gems/dry-configurable)
18
19
  * [API documentation](http://rubydoc.info/gems/dry-configurable)
19
20
 
20
21
  ## Supported Ruby versions
21
22
 
22
23
  This library officially supports the following Ruby versions:
23
24
 
24
- * MRI >= `2.5`
25
- * jruby >= `9.2`
25
+ * MRI `>= 2.7.0`
26
+ * jruby `>= 9.3` (postponed until 2.7 is supported)
26
27
 
27
28
  ## License
28
29
 
@@ -1,35 +1,36 @@
1
1
  # frozen_string_literal: true
2
- # this file is managed by dry-rb/devtools project
3
2
 
4
- lib = File.expand_path('lib', __dir__)
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
5
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
- require 'dry/configurable/version'
7
+ require "dry/configurable/version"
7
8
 
8
9
  Gem::Specification.new do |spec|
9
- spec.name = 'dry-configurable'
10
+ spec.name = "dry-configurable"
10
11
  spec.authors = ["Andy Holland"]
11
12
  spec.email = ["andyholland1991@aol.com"]
12
- spec.license = 'MIT'
13
+ spec.license = "MIT"
13
14
  spec.version = Dry::Configurable::VERSION.dup
14
15
 
15
16
  spec.summary = "A mixin to add configuration functionality to your classes"
16
17
  spec.description = spec.summary
17
- spec.homepage = 'https://dry-rb.org/gems/dry-configurable'
18
+ spec.homepage = "https://dry-rb.org/gems/dry-configurable"
18
19
  spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-configurable.gemspec", "lib/**/*"]
19
- spec.bindir = 'bin'
20
+ spec.bindir = "bin"
20
21
  spec.executables = []
21
- spec.require_paths = ['lib']
22
+ spec.require_paths = ["lib"]
22
23
 
23
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
- spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-configurable/blob/master/CHANGELOG.md'
25
- spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-configurable'
26
- spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-configurable/issues'
24
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
25
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-configurable/blob/main/CHANGELOG.md"
26
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-configurable"
27
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-configurable/issues"
27
28
 
28
- spec.required_ruby_version = ">= 2.5.0"
29
+ spec.required_ruby_version = ">= 2.7.0"
29
30
 
30
31
  # to update dependencies edit project.yml
31
- spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
32
- spec.add_runtime_dependency "dry-core", "~> 0.5", ">= 0.5.0"
32
+ spec.add_runtime_dependency "dry-core", "~> 0.6"
33
+ spec.add_runtime_dependency "zeitwerk", "~> 2.6"
33
34
 
34
35
  spec.add_development_dependency "bundler"
35
36
  spec.add_development_dependency "rake"
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
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'
3
+ require "set"
9
4
 
10
5
  module Dry
11
6
  module Configurable
@@ -13,30 +8,40 @@ module Dry
13
8
  include Methods
14
9
 
15
10
  # @api private
16
- def inherited(klass)
11
+ def inherited(subclass)
17
12
  super
18
13
 
19
- parent_settings = (respond_to?(:config) ? config._settings : _settings)
14
+ subclass.instance_variable_set(:@__config_extension__, __config_extension__)
15
+
16
+ new_settings = _settings.dup
17
+ subclass.instance_variable_set(:@_settings, new_settings)
20
18
 
21
- klass.instance_variable_set('@_settings', parent_settings)
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
22
25
  end
23
26
 
24
27
  # Add a setting to the configuration
25
28
  #
26
- # @param [Mixed] key
29
+ # @param [Mixed] name
27
30
  # The accessor key for the configuration value
28
31
  # @param [Mixed] default
29
- # The default config value
30
- #
32
+ # Default value for the setting
33
+ # @param [#call] constructor
34
+ # Transformation given value will go through
35
+ # @param [Boolean] reader
36
+ # Whether a reader accessor must be created
31
37
  # @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
38
+ # A block can be given to add nested settings.
34
39
  #
35
40
  # @return [Dry::Configurable::Config]
36
41
  #
37
42
  # @api public
38
- def setting(*args, &block)
39
- setting = __config_dsl__.setting(*args, &block)
43
+ def setting(*args, **options, &block)
44
+ setting = __config_dsl__.setting(*args, **options, &block)
40
45
 
41
46
  _settings << setting
42
47
 
@@ -51,7 +56,7 @@ module Dry
51
56
  #
52
57
  # @api public
53
58
  def settings
54
- @settings ||= Set[*_settings.map(&:name)]
59
+ Set[*_settings.map(&:name)]
55
60
  end
56
61
 
57
62
  # Return declared settings
@@ -69,12 +74,25 @@ module Dry
69
74
  #
70
75
  # @api public
71
76
  def config
72
- @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__
73
88
  end
74
89
 
75
90
  # @api private
76
91
  def __config_dsl__
77
- @dsl ||= DSL.new
92
+ @__config_dsl__ ||= DSL.new(
93
+ config_class: __config_extension__.config_class,
94
+ default_undefined: __config_extension__.default_undefined
95
+ )
78
96
  end
79
97
 
80
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
@@ -23,22 +20,18 @@ module Dry
23
20
  public_send(:"visit_#{type}", rest)
24
21
  end
25
22
 
26
- # @api private
27
- def visit_constructor(node)
28
- setting, constructor = node
29
- visit(setting).with(constructor: constructor)
30
- end
31
-
32
23
  # @api private
33
24
  def visit_setting(node)
34
- name, default, opts = node
35
- Setting.new(name, **opts, default: default)
25
+ name, opts = node
26
+ Setting.new(name, **opts)
36
27
  end
37
28
 
38
29
  # @api private
39
30
  def visit_nested(node)
40
31
  parent, children = node
41
- visit(parent).nested(call(children))
32
+ name, opts = parent[1]
33
+
34
+ Setting.new(name, **opts, children: Settings.new(call(children)))
42
35
  end
43
36
  end
44
37
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "dry/core/constants"
4
+ require "set"
4
5
 
5
- require 'dry/core/equalizer'
6
-
7
- require 'dry/configurable/constants'
8
- require 'dry/configurable/errors'
6
+ require "dry/core/equalizer"
9
7
 
10
8
  module Dry
11
9
  module Configurable
@@ -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)
26
- @_settings = settings.dup
27
- @_resolved = Concurrent::Map.new
27
+ def initialize(settings, values: {})
28
+ @_settings = settings
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,21 +70,34 @@ 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
52
87
  #
53
- # @param values [Hash] A hash with new values
88
+ # @param values [Hash, #to_hash] A hash with new values
54
89
  #
55
90
  # @return [Config]
56
91
  #
57
92
  # @api public
58
93
  def update(values)
59
94
  values.each do |key, value|
60
- case value
61
- when Hash
62
- self[key].update(value)
95
+ if self[key].is_a?(self.class)
96
+ unless value.respond_to?(:to_hash)
97
+ raise ArgumentError, "#{value.inspect} is not a valid setting value"
98
+ end
99
+
100
+ self[key].update(value.to_hash)
63
101
  else
64
102
  self[key] = value
65
103
  end
@@ -67,62 +105,94 @@ module Dry
67
105
  self
68
106
  end
69
107
 
70
- # 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.
71
130
  #
72
131
  # @return [Hash]
73
132
  #
74
133
  # @api public
75
134
  def values
76
- _settings
77
- .map { |setting| [setting.name, setting.value] }
78
- .map { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] }
79
- .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
80
139
  end
81
- alias_method :to_h, :values
82
- alias_method :to_hash, :values
83
140
 
84
- # @api private
85
- def finalize!
86
- _settings.freeze
87
- freeze
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] }
88
149
  end
89
150
 
90
151
  # @api private
91
- def pristine
92
- self.class.new(_settings.pristine)
152
+ def finalize!(freeze_values: false)
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
+
161
+ freeze
93
162
  end
94
163
 
95
164
  # @api private
96
- def respond_to_missing?(meth, include_private = false)
97
- super || _settings.key?(resolve(meth))
165
+ def pristine
166
+ self.class.new(_settings)
98
167
  end
99
168
 
100
169
  private
101
170
 
102
- # @api private
103
- def method_missing(meth, *args)
104
- setting = _settings[resolve(meth)]
171
+ def method_missing(name, *args)
172
+ setting_name = setting_name_from_method(name)
173
+ setting = _settings[setting_name]
105
174
 
106
175
  super unless setting
107
176
 
108
- if setting.writer?(meth)
109
- raise FrozenConfig, 'Cannot modify frozen config' if frozen?
110
-
111
- _settings << setting.with(input: args[0])
177
+ if name.end_with?("=")
178
+ self[setting_name] = args[0]
112
179
  else
113
- setting.value
180
+ self[setting_name]
114
181
  end
115
182
  end
116
183
 
117
- # @api private
118
- def resolve(meth)
119
- _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
120
186
  end
121
187
 
122
- # @api private
123
- def initialize_copy(source)
124
- super
125
- @_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
+ }
126
196
  end
127
197
  end
128
198
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/constants'
3
+ require "dry/core/constants"
4
4
 
5
5
  module Dry
6
6
  # Shared constants
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
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'
3
+ require "dry/core/deprecations"
8
4
 
9
5
  module Dry
10
6
  module Configurable
@@ -14,49 +10,67 @@ module Dry
14
10
  class DSL
15
11
  VALID_NAME = /\A[a-z_]\w*\z/i.freeze
16
12
 
17
- # @api private
18
13
  attr_reader :compiler
19
14
 
20
- # @api private
21
15
  attr_reader :ast
22
16
 
23
- # @api private
24
- def initialize(&block)
17
+ attr_reader :options
18
+
19
+ def initialize(**options, &block)
25
20
  @compiler = Compiler.new
26
21
  @ast = []
22
+ @options = options
27
23
  instance_exec(&block) if block
28
24
  end
29
25
 
30
- # Register a new setting node and compile it into a setting object
26
+ # Registers a new setting node and compile it into a setting object
31
27
  #
32
28
  # @see ClassMethods.setting
33
- # @api public
29
+ # @api private
34
30
  # @return Setting
35
- def setting(name, *args, &block)
31
+ def setting(name, **options, &block) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
36
32
  unless VALID_NAME.match?(name.to_s)
37
33
  raise ArgumentError, "#{name} is not a valid setting name"
38
34
  end
39
35
 
40
- args = Args.new(args)
36
+ ensure_valid_options(options)
41
37
 
42
- args.ensure_valid_options
38
+ options = {default: default, config_class: config_class, **options}
43
39
 
44
- default, opts = args
45
-
46
- node = [:setting, [name.to_sym, default, opts == default ? EMPTY_HASH : opts]]
40
+ node = [:setting, [name.to_sym, options]]
47
41
 
48
42
  if block
49
- if block.arity.zero?
50
- ast << [:nested, [node, DSL.new(&block).ast]]
51
- else
52
- ast << [:constructor, [node, block]]
53
- end
43
+ ast << [:nested, [node, DSL.new(&block).ast]]
54
44
  else
55
45
  ast << node
56
46
  end
57
47
 
58
48
  compiler.visit(ast.last)
59
49
  end
50
+
51
+ def config_class
52
+ options[:config_class]
53
+ end
54
+
55
+ def default
56
+ options[:default_undefined] ? Undefined : nil
57
+ end
58
+
59
+ private
60
+
61
+ def ensure_valid_options(options)
62
+ return if options.none?
63
+
64
+ invalid_keys = options.keys - Setting::OPTIONS
65
+
66
+ raise ArgumentError, "Invalid options: #{invalid_keys.inspect}" unless invalid_keys.empty?
67
+ end
68
+
69
+ # Returns a tuple of valid and invalid options hashes derived from the options hash
70
+ # given to the setting
71
+ def valid_and_invalid_options(options)
72
+ options.partition { |k, _| Setting::OPTIONS.include?(k) }.map(&:to_h)
73
+ end
60
74
  end
61
75
  end
62
76
  end
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable/config'
4
- require 'dry/configurable/methods'
3
+ require "dry/configurable/config"
4
+ require "dry/configurable/methods"
5
5
 
6
6
  module Dry
7
7
  module Configurable
@@ -12,9 +12,11 @@ module Dry
12
12
  module Initializer
13
13
  # @api private
14
14
  def initialize(*)
15
- @config = Config.new(self.class._settings.dup)
15
+ @config = self.class.__config_build__(self.class._settings)
16
+
16
17
  super
17
18
  end
19
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
18
20
  end
19
21
 
20
22
  # Instance-level API when `Dry::Configurable` is included in a class
@@ -33,9 +35,7 @@ module Dry
33
35
  # Finalize the config and freeze the object
34
36
  #
35
37
  # @api public
36
- def finalize!
37
- return self if frozen?
38
-
38
+ def finalize!(freeze_values: false)
39
39
  super
40
40
  freeze
41
41
  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
@@ -10,7 +8,7 @@ module Dry
10
8
  module Methods
11
9
  # @api public
12
10
  def configure(&block)
13
- raise FrozenConfig, 'Cannot modify frozen config' if frozen?
11
+ raise FrozenConfig, "Cannot modify frozen config" if frozen?
14
12
 
15
13
  yield(config) if block
16
14
  self
@@ -21,10 +19,8 @@ module Dry
21
19
  # @return [Dry::Configurable::Config]
22
20
  #
23
21
  # @api public
24
- def finalize!
25
- return self if config.frozen?
26
-
27
- config.finalize!
22
+ def finalize!(freeze_values: false)
23
+ config.finalize!(freeze_values: freeze_values)
28
24
  self
29
25
  end
30
26
  end
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
3
+ require "set"
4
4
 
5
- require 'dry/core/equalizer'
6
-
7
- require 'dry/configurable/constants'
8
- require 'dry/configurable/config'
5
+ require "dry/core/equalizer"
9
6
 
10
7
  module Dry
11
8
  module Configurable
@@ -13,9 +10,9 @@ module Dry
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,49 +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
- @input = input
66
53
  @default = default
54
+ @cloneable = children.any? || options.fetch(:cloneable) {
55
+ Setting.cloneable_value?(default)
56
+ }
57
+ @constructor = constructor
58
+ @children = children
67
59
  @options = options
68
-
69
- evaluate if input_defined?
70
- end
71
-
72
- # @api private
73
- def input_defined?
74
- !input.equal?(Undefined)
75
- end
76
-
77
- # @api private
78
- def value
79
- @value ||= evaluate
80
- end
81
-
82
- # @api private
83
- def evaluated?
84
- instance_variable_defined?(:@value)
85
- end
86
-
87
- # @api private
88
- def nested(settings)
89
- Nested.new(name, input: settings, **options)
90
- end
91
-
92
- # @api private
93
- def pristine
94
- with(input: Undefined)
95
- end
96
-
97
- # @api private
98
- def with(new_opts)
99
- self.class.new(name, input: input, default: default, **options, **new_opts)
100
- end
101
-
102
- # @api private
103
- def constructor
104
- options[:constructor] || DEFAULT_CONSTRUCTOR
105
60
  end
106
61
 
107
62
  # @api private
@@ -109,43 +64,22 @@ module Dry
109
64
  options[:reader].equal?(true)
110
65
  end
111
66
 
112
- # @api private
113
- def writer?(meth)
114
- writer_name.equal?(meth)
115
- end
116
-
117
67
  # @api private
118
68
  def cloneable?
119
- if options.key?(:cloneable)
120
- # Return cloneable option if explicitly set
121
- options[:cloneable]
122
- else
123
- # Otherwise, infer cloneable from any of the input, default, or value
124
- Setting.cloneable_value?(input) || Setting.cloneable_value?(default) || (
125
- evaluated? && Setting.cloneable_value?(value)
126
- )
127
- end
69
+ cloneable
128
70
  end
129
71
 
130
- private
131
-
132
72
  # @api private
133
- def initialize_copy(source)
134
- super
135
-
136
- @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)
137
79
 
138
- if source.cloneable?
139
- @input = source.input.dup
140
- @default = source.default.dup
141
- @value = source.value.dup if source.evaluated?
80
+ cloneable? ? value.dup : value
142
81
  end
143
82
  end
144
-
145
- # @api private
146
- def evaluate
147
- @value = constructor[Undefined.coalesce(input, default, nil)]
148
- end
149
83
  end
150
84
  end
151
85
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
4
-
5
- require 'dry/core/equalizer'
6
- require 'dry/configurable/constants'
3
+ require "dry/core/equalizer"
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,31 +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))
46
+ settings.each_value(&block)
55
47
  end
56
48
 
57
49
  private
58
50
 
59
- # @api private
60
51
  def initialize_copy(source)
61
- initialize_elements(source.map(&:dup))
62
- end
63
-
64
- # @api private
65
- def initialize_elements(elements)
66
- @elements = elements.each_with_object(Concurrent::Map.new) { |s, m|
67
- m[s.name] = s
68
- }
52
+ @settings = source.settings.dup
69
53
  end
70
54
  end
71
55
  end
@@ -3,6 +3,6 @@
3
3
  module Dry
4
4
  module Configurable
5
5
  # @api public
6
- VERSION = '0.12.1'
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/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
- require 'dry/configurable/errors'
5
+ require "dry/core/constants"
6
+ require "dry/configurable/constants"
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
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable'
3
+ require "dry/configurable"
metadata CHANGED
@@ -1,49 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-configurable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
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: 2021-02-15 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.5'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.5.0
33
+ version: '2.6'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
- version: '0.5'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.5.0
40
+ version: '2.6'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: bundler
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -104,8 +98,8 @@ files:
104
98
  - lib/dry/configurable/config.rb
105
99
  - lib/dry/configurable/constants.rb
106
100
  - lib/dry/configurable/dsl.rb
107
- - lib/dry/configurable/dsl/args.rb
108
101
  - lib/dry/configurable/errors.rb
102
+ - lib/dry/configurable/extension.rb
109
103
  - lib/dry/configurable/instance_methods.rb
110
104
  - lib/dry/configurable/methods.rb
111
105
  - lib/dry/configurable/setting.rb
@@ -117,7 +111,7 @@ licenses:
117
111
  - MIT
118
112
  metadata:
119
113
  allowed_push_host: https://rubygems.org
120
- changelog_uri: https://github.com/dry-rb/dry-configurable/blob/master/CHANGELOG.md
114
+ changelog_uri: https://github.com/dry-rb/dry-configurable/blob/main/CHANGELOG.md
121
115
  source_code_uri: https://github.com/dry-rb/dry-configurable
122
116
  bug_tracker_uri: https://github.com/dry-rb/dry-configurable/issues
123
117
  post_install_message:
@@ -128,14 +122,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
122
  requirements:
129
123
  - - ">="
130
124
  - !ruby/object:Gem::Version
131
- version: 2.5.0
125
+ version: 2.7.0
132
126
  required_rubygems_version: !ruby/object:Gem::Requirement
133
127
  requirements:
134
128
  - - ">="
135
129
  - !ruby/object:Gem::Version
136
130
  version: '0'
137
131
  requirements: []
138
- rubygems_version: 3.1.4
132
+ rubygems_version: 3.1.6
139
133
  signing_key:
140
134
  specification_version: 4
141
135
  summary: A mixin to add configuration functionality to your classes
@@ -1,58 +0,0 @@
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