dry-configurable 0.12.1 → 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: 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