Merge branch 'main' into master

This commit is contained in:
Peter Solnica 2022-07-01 11:12:13 +02:00 committed by GitHub
commit a44afe8999
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 591 additions and 219 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
github: [jodosha, solnic, timriley]
github: hanami

View File

@ -15,7 +15,7 @@ on:
- "project.yml"
pull_request:
branches:
- master
- main
create:
jobs:
@ -26,18 +26,18 @@ jobs:
fail-fast: false
matrix:
ruby:
- "3.1"
- "3.0"
- "2.7"
- "2.6"
include:
- ruby: "3.0"
- ruby: "3.1"
coverage: "true"
env:
COVERAGE: ${{matrix.coverage}}
COVERAGE_TOKEN: ${{secrets.CODACY_PROJECT_TOKEN}}
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Install package dependencies
run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
- name: Set up Ruby
@ -47,12 +47,6 @@ jobs:
bundler-cache: true
- name: Run all tests
run: bundle exec rake
- name: Run codacy-coverage-reporter
uses: codacy/codacy-coverage-reporter-action@master
if: env.COVERAGE == 'true' && env.COVERAGE_TOKEN != ''
with:
project-token: ${{secrets.CODACY_PROJECT_TOKEN}}
coverage-reports: coverage/coverage.xml
release:
runs-on: ubuntu-latest
if: contains(github.ref, 'tags') && github.event_name == 'create'
@ -61,13 +55,13 @@ jobs:
GITHUB_LOGIN: dry-bot
GITHUB_TOKEN: ${{secrets.GH_PAT}}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Install package dependencies
run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
ruby-version: 2.7
- name: Install dependencies
run: gem install ossy --no-document
- name: Trigger release workflow

View File

@ -1,4 +1,4 @@
# this file is managed by dry-rb/devtools project
# This file is synced from dry-rb/template-gem repo
name: docsite
@ -8,7 +8,7 @@ on:
- docsite/**
- .github/workflows/docsite.yml
branches:
- master
- main
- release-**
tags:
@ -24,7 +24,7 @@ jobs:
- name: Set up Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: "2.6.x"
ruby-version: "2.7.x"
- name: Set up git user
run: |
git config --local user.email "dry-bot@dry-rb.org"
@ -51,7 +51,7 @@ jobs:
git push --all "https://dry-bot:${{secrets.GH_PAT}}@github.com/$GITHUB_REPOSITORY.git"
git checkout master
git checkout main
else
echo "no need to update branches"
fi

View File

@ -6,7 +6,7 @@ on:
repository_dispatch:
push:
branches:
- "master"
- "main"
jobs:
main:
@ -17,7 +17,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
steps:
- name: Checkout ${{github.repository}}
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Checkout devtools
uses: actions/checkout@v2
with:
@ -30,7 +30,7 @@ jobs:
- name: Set up Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: "2.6"
ruby-version: "2.7"
- name: Install dependencies
run: gem install ossy --no-document
- name: Update changelog.yml from commit
@ -43,5 +43,5 @@ jobs:
git commit -m "[devtools] sync" || echo "nothing to commit"
- name: Push changes
run: |
git pull --rebase origin master
git push https://dry-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:master
git pull --rebase origin main
git push https://dry-bot:${{secrets.GH_PAT}}@github.com/${{github.repository}}.git HEAD:main

View File

@ -1,9 +1,10 @@
# This is a config synced from dry-rb/template-gem repo
AllCops:
TargetRubyVersion: 2.6
NewCops: enable
TargetRubyVersion: 2.7
NewCops: disable
Exclude:
- benchmarks/*.rb
- spec/support/coverage.rb
- spec/support/warnings.rb
- spec/support/rspec_options.rb
@ -56,6 +57,10 @@ Lint/SuppressedException:
Exclude:
- "spec/spec_helper.rb"
Lint/LiteralAsCondition:
Exclude:
- "spec/**/*.rb"
Naming/PredicateName:
Enabled: false
@ -67,9 +72,7 @@ Naming/MethodName:
Enabled: false
Naming/MethodParameterName:
Enabled: true
Exclude:
- "spec/**/*.rb"
Enabled: false
Naming/MemoizedInstanceVariableName:
Enabled: false
@ -134,6 +137,9 @@ Style/EachWithObject:
Style/FormatString:
Enabled: false
Style/FormatStringToken:
Enabled: false
Style/GuardClause:
Enabled: false
@ -174,10 +180,58 @@ Style/MultipleComparison:
Style/Next:
Enabled: false
Style/AccessorGrouping:
Enabled: false
Style/EmptyLiteral:
Enabled: false
Style/Semicolon:
Exclude:
- "spec/**/*.rb"
Style/HashAsLastArrayItem:
Exclude:
- "spec/**/*.rb"
Style/CaseEquality:
Exclude:
- "lib/dry/monads/**/*.rb"
- "lib/dry/struct/**/*.rb"
- "lib/dry/types/**/*.rb"
- "spec/**/*.rb"
Style/ExplicitBlockArgument:
Exclude:
- "lib/dry/types/**/*.rb"
Style/CombinableLoops:
Enabled: false
Style/EmptyElse:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/MultilineBlockChain:
Enabled: false
Style/NumberedParametersLimit:
Max: 2
Lint/UnusedBlockArgument:
Exclude:
- "spec/**/*.rb"
Lint/Debugger:
Exclude:
- "bin/console"
Lint/BinaryOperatorWithIdenticalOperands:
Exclude:
- "spec/**/*.rb"
Metrics/ParameterLists:
Exclude:
- "spec/**/*.rb"
@ -186,6 +240,23 @@ Lint/EmptyBlock:
Exclude:
- "spec/**/*.rb"
Lint/UselessMethodDefinition:
Exclude:
- "spec/**/*.rb"
Lint/SelfAssignment:
Enabled: false
Lint/EmptyClass:
Enabled: false
Naming/ConstantName:
Exclude:
- "spec/**/*.rb"
Naming/VariableNumber:
Exclude:
- "spec/**/*.rb"
Naming/BinaryOperatorParameterName:
Enabled: false

View File

@ -1,19 +1,81 @@
<!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
## unreleased
## 0.15.0 2022-04-21
### Fixed
- Fix `ArgumentError` for classes including `Dry::Configurable` whose `initializer` has required kwargs. (#113 by @timriley)
- Remove implicit `to_hash` conversion from `Config`. (#114 by @timriley)
### Changed
- Deprecate constructor as a block. Providing `constructor:` kwarg is now the only accepted way. (#111 by @waiting-for-dev & @timriley)
- Use `default:` kwarg instead of second positional argument for the default value. (#112 by @waiting-for-dev & @timriley)
- 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)
[Compare v0.12.1...master](https://github.com/dry-rb/dry-configurable/compare/v0.12.1...master)
```ruby
class MyConfigurable
include Dry::Configurable
setting :db, default: "postgre"
end
my_obj = MyConfigurable.new
my_obj.finalize!(freeze_values: true)
my_obj.config.db << "sql" # Will raise FrozenError
```
- `Dry::Configurable::Config#update` will set hashes as values for non-nested settings (#131 by @ojab)
```ruby
class MyConfigurable
extend Dry::Configurable
setting :sslcert, constructor: ->(v) { v&.values_at(:pem, :pass)&.join }
end
MyConfigurable.config.update(sslcert: {pem: "cert", pass: "qwerty"})
MyConfigurable.config.sslcert # => "certqwerty"
```
- `Dry::Configurable::Config#update` will accept any values implicitly convertible to hash via `#to_hash` (#133 by @timriley)
[Compare v0.14.0...v0.15.0](https://github.com/dry-rb/dry-configurable/compare/v0.14.0...v0.15.0)
## 0.14.0 2022-01-14
### Changed
- Settings defined after an access to `config` will still be made available on that `config`. (#130 by @timriley)
- Cloneable settings are cloned immediately upon assignment. (#130 by @timriley)
- 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)
[Compare v0.13.0...v0.14.0](https://github.com/dry-rb/dry-configurable/compare/v0.13.0...v0.14.0)
## 0.13.0 2021-09-12
### Added
- 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)
```ruby
Dry::Configurable.warn_on_setting_constructor_block false
Dry::Configurable.warn_on_setting_positional_default false
```
### Fixed
- Fixed `ArgumentError` for classes including `Dry::Configurable` whose `initializer` has required kwargs. (#113 by @timriley)
### Changed
- Deprecated the setting constructor provided as a block. Provide it via the `constructor:` keyword argument instead. (#111 by @waiting-for-dev & @timriley)
```ruby
setting :path, constructor: -> path { Pathname(path) }
```
- 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)
```ruby
setting :path, default: "some/default/path"
```
- [BREAKING] Removed implicit `to_hash` conversion from `Config`. (#114 by @timriley)
[Compare v0.12.1...v0.13.0](https://github.com/dry-rb/dry-configurable/compare/v0.12.1...v0.13.0)
## 0.12.1 2021-02-15

View File

@ -1,94 +0,0 @@
## 0.9.0 - 2019-11-06
## Fixed
- Support for reserved names in settings. Some Kernel methods (`public_send` and `class` specifically) are not available if you use access settings via method call. Same for methods of the `Config` class. You can still access them with `[]` and `[]=`. Ruby keywords are fully supported. Invalid names containing special symbols (including `!` and `?`) are rejected. Note that these changes don't affect the `reader` option, if you define a setting named `:class` and pass `reader: true` ... well ... (flash-gordon)
- Settings can be redefined in subclasses without a warning about overriding exsting methods (flash-gordon)
- Fix warnings about using keyword arguments in 2.7 (koic)
[Compare v0.8.3...v0.9.0](https://github.com/dry-rb/dry-configurable/compare/v0.8.3...v0.9.0)
## 0.8.3 - 2019-05-29
## Fixed
- `Configurable#dup` and `Configurable#clone` make a copy of instance-level config so that it doesn't get mutated/shared across instances (flash-gordon)
[Compare v0.8.2...v0.8.3](https://github.com/dry-rb/dry-configurable/compare/v0.8.2...v0.8.3)
## 0.8.2 - 2019-02-25
## Fixed
- Test interface support for modules ([Neznauy](https://github.com/Neznauy))
[Compare v0.8.1...v0.8.2](https://github.com/dry-rb/dry-configurable/compare/v0.8.1...v0.8.2)
## 0.8.1 - 2019-02-06
## Fixed
- `.configure` doesn't require a block, this makes the behavior consistent with the previous versions ([flash-gordon](https://github.com/flash-gordon))
[Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-configurable/compare/v0.8.0...v0.8.1)
## 0.8.0 - 2019-02-05
## Fixed
- A number of bugs related to inheriting settings from parent class were fixed. Ideally, new behavior will be :100: predictable but if you observe any anomaly, please report ([flash-gordon](https://github.com/flash-gordon))
## Added
- Support for instance-level configuration landed. For usage, `include` the module instead of extending ([flash-gordon](https://github.com/flash-gordon))
```ruby
class App
include Dry::Configurable
setting :database
end
production = App.new
production.config.database = ENV['DATABASE_URL']
production.finalize!
development = App.new
development.config.database = 'jdbc:sqlite:memory'
development.finalize!
```
- Config values can be set from a hash with `.update`. Nested settings are supported ([flash-gordon](https://github.com/flash-gordon))
```ruby
class App
extend Dry::Configurable
setting :db do
setting :host
setting :port
end
config.update(YAML.load(File.read("config.yml")))
end
```
## Changed
- [BREAKING] Minimal supported Ruby version is set to 2.3 ([flash-gordon](https://github.com/flash-gordon))
[Compare v0.7.0...v0.8.0](https://github.com/dry-rb/dry-configurable/compare/v0.7.0...v0.8.0)
## 0.7.0 - 2017-04-25
## Added
- Introduce `Configurable.finalize!` which freezes config and its dependencies ([qcam](https://github.com/qcam))
## Fixed
- Allow for boolean false as default setting value ([yuszuv](https://github.com/yuszuv))
- Convert nested configs to nested hashes with `Config#to_h` ([saverio-kantox](https://github.com/saverio-kantox))
- Disallow modification on frozen config ([qcam](https://github.com/qcam))
[Compare v0.6.2...v0.7.0](https://github.com/dry-rb/dry-configurable/compare/v0.6.2...v0.7.0)

10
Gemfile
View File

@ -1,16 +1,16 @@
# frozen_string_literal: true
source 'https://rubygems.org'
source "https://rubygems.org"
eval_gemfile 'Gemfile.devtools'
eval_gemfile "Gemfile.devtools"
gemspec
group :benchmarks do
gem 'benchmark-ips'
gem "benchmark-ips"
end
group :tools do
gem 'hotch'
gem 'pry-byebug', platform: :mri
gem "hotch", platform: :mri
gem "pry-byebug", platform: :mri
end

View File

@ -13,6 +13,5 @@ group :test do
end
group :tools do
# this is the same version that we use on codacy
gem "rubocop", "1.16.0"
gem "rubocop", "~> 1.27.0"
end

View File

@ -11,7 +11,7 @@
[![CI Status](https://github.com/dry-rb/dry-configurable/workflows/ci/badge.svg)][actions]
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/0276a97990e04eb0ac722b3e7f3620b5)][codacy]
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/0276a97990e04eb0ac722b3e7f3620b5)][codacy]
[![Inline docs](http://inch-ci.org/github/dry-rb/dry-configurable.svg?branch=master)][inchpages]
[![Inline docs](http://inch-ci.org/github/dry-rb/dry-configurable.svg?branch=main)][inchpages]
## Links
@ -22,8 +22,8 @@
This library officially supports the following Ruby versions:
* MRI `>= 2.6.0`
* ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
* MRI `>= 2.7.0`
* jruby `>= 9.3` (postponed until 2.7 is supported)
## License

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true
require 'rspec/core/rake_task'
require "rspec/core/rake_task"
desc 'Run all specs in spec directory'
desc "Run all specs in spec directory"
RSpec::Core::RakeTask.new(:spec)
task default: :spec

View File

@ -1,13 +1,13 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/setup'
require 'dry/configurable'
require "bundler/setup"
require "dry/configurable"
begin
require 'pry-byebug'
require "pry-byebug"
Pry.start
rescue LoadError
require 'irb'
require "irb"
IRB.start
end

View File

@ -1,17 +1,74 @@
---
- version: unreleased
- version: 0.15.0
summary:
date:
fixed:
- 'Fix `ArgumentError` for classes including `Dry::Configurable` whose
`initializer` has required kwargs. (#113 by @timriley)'
- 'Remove implicit `to_hash` conversion from `Config`. (#114 by @timriley)'
added:
date: '2022-04-21'
changed:
- 'Deprecate constructor as a block. Providing `constructor:` kwarg is now
the only accepted way. (#111 by @waiting-for-dev & @timriley)'
- 'Use `default:` kwarg instead of second positional argument for the
default value. (#112 by @waiting-for-dev & @timriley)'
- |-
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)
```ruby
class MyConfigurable
include Dry::Configurable
setting :db, default: "postgre"
end
my_obj = MyConfigurable.new
my_obj.finalize!(freeze_values: true)
my_obj.config.db << "sql" # Will raise FrozenError
```
- |-
`Dry::Configurable::Config#update` will set hashes as values for non-nested settings (#131 by @ojab)
```ruby
class MyConfigurable
extend Dry::Configurable
setting :sslcert, constructor: ->(v) { v&.values_at(:pem, :pass)&.join }
end
MyConfigurable.config.update(sslcert: {pem: "cert", pass: "qwerty"})
MyConfigurable.config.sslcert # => "certqwerty"
```
- |-
`Dry::Configurable::Config#update` will accept any values implicitly convertible to hash via `#to_hash` (#133 by @timriley)
- version: 0.14.0
summary:
date: '2022-01-14'
changed:
- |-
Settings defined after an access to `config` will still be made available on that `config`. (#130 by @timriley)
- |-
Cloneable settings are cloned immediately upon assignment. (#130 by @timriley)
- |-
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)
- version: 0.13.0
summary:
date: '2021-09-12'
fixed:
- Fixed `ArgumentError` for classes including `Dry::Configurable` whose `initializer` has required kwargs. (#113 by @timriley)
added:
- |-
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)
```ruby
Dry::Configurable.warn_on_setting_constructor_block false
Dry::Configurable.warn_on_setting_positional_default false
```
changed:
- |-
Deprecated the setting constructor provided as a block. Provide it via the `constructor:` keyword argument instead. (#111 by @waiting-for-dev & @timriley)
```ruby
setting :path, constructor: -> path { Pathname(path) }
```
- |-
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)
```ruby
setting :path, default: "some/default/path"
```
- '[BREAKING] Removed implicit `to_hash` conversion from `Config`. (#114 by @timriley)'
- version: 0.12.1
summary:
date: '2021-02-15'

View File

@ -26,17 +26,17 @@ class App
# Pass a block for nested configuration (works to any depth)
setting :database do
# Can pass a default value
setting :dsn, 'sqlite:memory'
setting :dsn, default: 'sqlite:memory'
end
# Defaults to nil if no default value is given
setting :adapter
# Construct values
setting(:path, 'test') { |value| Pathname(value) }
setting :path, default: 'test', constructor: proc { |value| Pathname(value) }
# Passing the reader option as true will create attr_reader method for the class
setting :pool, 5, reader: true
setting :pool, default: 5, reader: true
# Passing the reader attributes works with nested configuration
setting :uploader, reader: true do
setting :bucket, 'dev'
setting :bucket, default: 'dev'
end
end

View File

@ -22,11 +22,11 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-configurable/blob/master/CHANGELOG.md"
spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-configurable/blob/main/CHANGELOG.md"
spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-configurable"
spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-configurable/issues"
spec.required_ruby_version = ">= 2.6.0"
spec.required_ruby_version = ">= 2.7.0"
# to update dependencies edit project.yml
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"

View File

@ -13,12 +13,11 @@ module Dry
include Methods
# @api private
def inherited(klass)
def inherited(subclass)
super
parent_settings = (respond_to?(:config) ? config._settings : _settings)
klass.instance_variable_set("@_settings", parent_settings)
subclass.instance_variable_set("@_settings", _settings.dup)
subclass.instance_variable_set("@_config", config.dup) if respond_to?(:config)
end
# Add a setting to the configuration
@ -71,12 +70,17 @@ module Dry
#
# @api public
def config
# The _settings provided to the Config remain shared between the class and the
# Config. This allows settings defined _after_ accessing the config to become
# available in subsequent accesses to the config. The config is duped when
# subclassing to ensure it remains distinct between subclasses and parent classes
# (see `.inherited` above).
@config ||= Config.new(_settings)
end
# @api private
def __config_dsl__
@dsl ||= DSL.new
@__config_dsl__ ||= DSL.new
end
# @api private

View File

@ -23,7 +23,7 @@ module Dry
# @api private
def initialize(settings)
@_settings = settings.dup
@_settings = settings
@_resolved = Concurrent::Map.new
end
@ -50,16 +50,19 @@ module Dry
# Update config with new values
#
# @param values [Hash] A hash with new values
# @param values [Hash, #to_hash] A hash with new values
#
# @return [Config]
#
# @api public
def update(values)
values.each do |key, value|
case value
when Hash
self[key].update(value)
if self[key].is_a?(self.class)
unless value.respond_to?(:to_hash)
raise ArgumentError, "#{value.inspect} is not a valid setting value"
end
self[key].update(value.to_hash)
else
self[key] = value
end
@ -81,8 +84,8 @@ module Dry
alias_method :to_h, :values
# @api private
def finalize!
_settings.freeze
def finalize!(freeze_values: false)
_settings.finalize!(freeze_values: freeze_values)
freeze
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require "dry/configurable/constants"
require "dry/configurable/flags"
require "dry/configurable/setting"
require "dry/configurable/settings"
require "dry/configurable/compiler"
@ -27,33 +28,105 @@ module Dry
instance_exec(&block) if block
end
# Register a new setting node and compile it into a setting object
# Registers a new setting node and compile it into a setting object
#
# @see ClassMethods.setting
# @api public
# @api private
# @return Setting
def setting(name, default = Undefined, **options, &block)
def setting(name, default = Undefined, **options, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
unless VALID_NAME.match?(name.to_s)
raise ArgumentError, "#{name} is not a valid setting name"
end
if default != Undefined
Dry::Core::Deprecations.announce(
"default value as positional argument to settings",
"Provide a `default:` keyword argument instead",
tag: "dry-configurable",
uplevel: 2
)
if Dry::Configurable.warn_on_setting_positional_default
Dry::Core::Deprecations.announce(
"default value as positional argument to settings",
"Provide a `default:` keyword argument instead",
tag: "dry-configurable",
uplevel: 2
)
end
options = options.merge(default: default)
end
if RUBY_VERSION < "3.0" &&
default == Undefined &&
(valid_opts, invalid_opts = valid_and_invalid_options(options)) &&
invalid_opts.any? &&
valid_opts.none?
# In Ruby 2.6 and 2.7, when a hash is given as the second positional argument
# (i.e. the hash is intended to be the setting's default value), and there are
# no other keyword arguments given, the hash is assigned to the `options`
# variable instead of `default`.
#
# For example, for this setting:
#
# setting :hash_setting, {my_hash: true}
#
# We'll have a `default` of `Undefined` and an `options` of `{my_hash: true}`
#
# If any additional keyword arguments are provided, e.g.:
#
# setting :hash_setting, {my_hash: true}, reader: true
#
# Then we'll have a `default` of `{my_hash: true}` and an `options` of `{reader:
# true}`, which is what we want.
#
# To work around that first case and ensure our (deprecated) backwards
# compatibility holds for Ruby 2.6 and 2.7, we extract all invalid options from
# `options`, and if there are no remaining valid options (i.e. if there were no
# keyword arguments given), then we can infer the invalid options to be a
# default hash value for the setting.
#
# This approach also preserves the behavior of raising an ArgumentError when a
# distinct hash is _not_ intentionally provided as the second positional
# argument (i.e. it's not enclosed in braces), and instead invalid keyword
# arguments are given alongside valid ones. So this setting:
#
# setting :some_setting, invalid_option: true, reader: true
#
# Would raise an ArgumentError as expected.
#
# However, the one case we can't catch here is when invalid options are supplied
# without hash literal braces, but there are no other keyword arguments
# supplied. In this case, a setting like:
#
# setting :hash_setting, my_hash: true
#
# Is parsed identically to the first case described above:
#
# setting :hash_setting, {my_hash: true}
#
# So in both of these cases, the default value will become `{my_hash: true}`. We
# consider this unlikely to be a problem in practice, since users are not likely
# to be providing invalid options to `setting` and expecting them to be ignored.
# Additionally, the deprecation messages will make the new behavior obvious, and
# encourage the users to upgrade their setting definitions.
if Dry::Configurable.warn_on_setting_positional_default
Dry::Core::Deprecations.announce(
"default value as positional argument to settings",
"Provide a `default:` keyword argument instead",
tag: "dry-configurable",
uplevel: 2
)
end
options = {default: invalid_opts}
end
if block && !block.arity.zero?
Dry::Core::Deprecations.announce(
"passing a constructor as a block",
"Provide a `constructor:` keyword argument instead",
tag: "dry-configurable",
uplevel: 2
)
if Dry::Configurable.warn_on_setting_constructor_block
Dry::Core::Deprecations.announce(
"passing a constructor as a block",
"Provide a `constructor:` keyword argument instead",
tag: "dry-configurable",
uplevel: 2
)
end
options = options.merge(constructor: block)
block = nil
end
@ -77,8 +150,15 @@ module Dry
return if options.none?
invalid_keys = options.keys - Setting::OPTIONS
raise ArgumentError, "Invalid options: #{invalid_keys.inspect}" unless invalid_keys.empty?
end
# Returns a tuple of valid and invalid options hashes derived from the options hash
# given to the setting
def valid_and_invalid_options(options)
options.partition { |k, _| Setting::OPTIONS.include?(k) }.map(&:to_h)
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require "dry/core/class_attributes"
module Dry
module Configurable
extend Core::ClassAttributes
# Set to false to suppress deprecation warning when a setting default is provided as a
# positional argument
defines :warn_on_setting_positional_default
warn_on_setting_positional_default true
# Set to false to suppress deprecation warning when a setting constructor is provided
# as a block
defines :warn_on_setting_constructor_block
warn_on_setting_constructor_block true
end
end

View File

@ -12,7 +12,11 @@ module Dry
module Initializer
# @api private
def initialize(*)
# Dup settings at time of initializing to ensure setting values are specific to
# this instance. This does mean that any settings defined on the class _after_
# initialization will not be available on the instance.
@config = Config.new(self.class._settings.dup)
super
end
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
@ -34,9 +38,7 @@ module Dry
# Finalize the config and freeze the object
#
# @api public
def finalize!
return self if frozen?
def finalize!(freeze_values: false)
super
freeze
end

View File

@ -21,10 +21,8 @@ module Dry
# @return [Dry::Configurable::Config]
#
# @api public
def finalize!
return self if config.frozen?
config.finalize!
def finalize!(freeze_values: false)
config.finalize!(freeze_values: freeze_values)
self
end
end

View File

@ -62,9 +62,17 @@ module Dry
def initialize(name, input: Undefined, default: Undefined, **options)
@name = name
@writer_name = :"#{name}="
@options = options
# Setting collections (see `Settings`) are shared between the configurable class
# and its `config` object, so for cloneable individual settings, we duplicate
# their _values_ as early as possible to ensure no impact from unintended mutation
@input = input
@default = default
@options = options
if cloneable?
@input = input.dup
@default = default.dup
end
evaluate if input_defined?
end
@ -76,8 +84,12 @@ module Dry
# @api private
def value
@value ||= evaluate
return @value if evaluated?
@value = constructor[Undefined.coalesce(input, default, nil)]
end
alias_method :evaluate, :value
private :evaluate
# @api private
def evaluated?
@ -94,6 +106,16 @@ module Dry
with(input: Undefined)
end
# @api private
def finalize!(freeze_values: false)
if value.is_a?(Config)
value.finalize!(freeze_values: freeze_values)
elsif freeze_values
value.freeze
end
freeze
end
# @api private
def with(new_opts)
self.class.new(name, input: input, default: default, **options, **new_opts)
@ -141,11 +163,6 @@ module Dry
@value = source.value.dup if source.evaluated?
end
end
# @api private
def evaluate
@value = constructor[Undefined.coalesce(input, default, nil)]
end
end
end
end

View File

@ -54,6 +54,12 @@ module Dry
self.class.new(map(&:pristine))
end
# @api private
def finalize!(freeze_values: false)
each { |element| element.finalize!(freeze_values: freeze_values) }
freeze
end
private
# @api private

View File

@ -3,6 +3,6 @@
module Dry
module Configurable
# @api public
VERSION = "0.13.0"
VERSION = "0.15.0"
end
end

View File

@ -35,6 +35,27 @@ RSpec.describe Dry::Configurable::Config do
expect(klass.config.db.user).to eql("jane")
expect(klass.config.db.pass).to eql("supersecret")
end
it "runs constructors" do
klass.setting :db do
setting :user, default: "root", constructor: ->(v) { v.upcase }
setting :sslcert, constructor: ->(v) { v&.values_at(:pem, :pass)&.join }
end
klass.config.update(db: {user: "jane", sslcert: {pem: "cert", pass: "qwerty"}})
expect(klass.config.db.user).to eql("JANE")
expect(klass.config.db.sslcert).to eql("certqwerty")
end
it "raises ArgumentError when setting value is not a Hash" do
klass.setting :db do
setting :user
end
expect { klass.config.update(db: "string") }
.to raise_error(ArgumentError, '"string" is not a valid setting value')
end
end
describe "#to_h" do

View File

@ -147,7 +147,7 @@ RSpec.describe Dry::Configurable, ".setting" do
context "with a ruby keyword" do
before do
klass.setting :if, true
klass.setting :if, default: true
end
it "works" do
@ -185,6 +185,15 @@ RSpec.describe Dry::Configurable, ".setting" do
include_context "configurable behavior"
specify "settings defined after accessing config are still available in the config" do
klass.setting :before, default: "defined before"
klass.config
klass.setting :after, default: "defined after"
expect(klass.config.before).to eq "defined before"
expect(klass.config.after).to eq "defined after"
end
context "with a subclass" do
let(:subclass) do
Class.new(klass)
@ -205,7 +214,7 @@ RSpec.describe Dry::Configurable, ".setting" do
it "allows defining more settings" do
klass.setting :db, default: "sqlite"
subclass.setting :username, "root"
subclass.setting :username, default: "root"
subclass.setting :password
subclass.config.password = "secret"
@ -225,7 +234,7 @@ RSpec.describe Dry::Configurable, ".setting" do
expect(subclass.settings).to eql(Set[:db])
end
it "configured parent copies config to the child" do
specify "configuring the parent before subclassing copies the config to the child" do
klass.setting :db
object.config.db = "mariadb"
@ -233,6 +242,16 @@ RSpec.describe Dry::Configurable, ".setting" do
expect(subclass.config.db).to eql("mariadb")
end
specify "configuring the parent after subclassing does not copy the config to the child" do
klass.setting :db
subclass = Class.new(klass)
object.config.db = "mariadb"
expect(subclass.config.db).to be nil
end
it "not configured parent does not set child config" do
klass.setting :db
@ -308,13 +327,13 @@ RSpec.describe Dry::Configurable, ".setting" do
end
it "defines a constructor that sets the config" do
klass.setting :db, "sqlite"
klass.setting :db, default: "sqlite"
expect(object.config.db).to eql("sqlite")
end
it "creates distinct setting values across instances" do
klass.setting(:path, "test", constructor: ->(m) { Pathname(m) })
klass.setting(:path, default: "test", constructor: ->(m) { Pathname(m) })
new_object = klass.new
@ -323,13 +342,29 @@ RSpec.describe Dry::Configurable, ".setting" do
expect(object.config.path).not_to be(new_object.config.path)
end
it "makes only settings defined before instantiation available" do
klass.setting :before, default: "defined before"
object_1 = klass.new
klass.setting :after, default: "defined after"
object_2 = klass.new
expect(object_1.config.before).to eq "defined before"
expect(object_1.config).not_to respond_to(:after)
expect(object_2.config.before).to eq "defined before"
expect(object_2.config.after).to eq "defined after"
end
shared_examples "copying" do
before do
klass.setting :env
klass.setting :db do
setting :user, "root"
setting :pass, "secret"
setting :user, default: "root".dup
setting :pass, default: "secret"
end
end
@ -365,7 +400,7 @@ RSpec.describe Dry::Configurable, ".setting" do
end
it "can be configured" do
klass.setting :db, "sqlite"
klass.setting :db, default: "sqlite"
object.configure do |config|
config.db = "mariadb"
@ -375,13 +410,35 @@ RSpec.describe Dry::Configurable, ".setting" do
end
it "can be finalized" do
klass.setting :db, "sqlite"
klass.setting :kafka, default: "kafka://127.0.0.1:9092"
object.finalize!
# becomes a no-op
object.finalize!
expect(object).to be_frozen
expect(object.config.db).to be_frozen
expect(object.config.db.user).not_to be_frozen
object.config.db.user << "foo"
expect(object.config.db.user).to eq("rootfoo")
# does not allow configure block anymore
expect { object.configure {} }.to raise_error(Dry::Configurable::FrozenConfig)
end
it "can be finalized with freezing values" do
klass.setting :kafka, "kafka://127.0.0.1:9092"
object.finalize!(freeze_values: true)
# becomes a no-op
object.finalize!(freeze_values: true)
expect(object).to be_frozen
expect(object.config.db).to be_frozen
expect(object.config.db.user).to be_frozen
expect { object.config.db.user << "foo" }.to raise_error(FrozenError)
# does not allow configure block anymore
expect { object.configure {} }.to raise_error(Dry::Configurable::FrozenConfig)
@ -389,7 +446,7 @@ RSpec.describe Dry::Configurable, ".setting" do
it "defines a reader shortcut for nested config" do
klass.setting :dsn, reader: true do
setting :pool, 5
setting :pool, default: 5
end
expect(object.dsn.pool).to be(5)
@ -398,10 +455,10 @@ RSpec.describe Dry::Configurable, ".setting" do
context "Test Interface" do
describe "reset_config" do
it "resets configuration to default values" do
klass.setting :dsn, nil
klass.setting :dsn, default: nil
klass.setting :pool do
setting :size, nil
setting :size, default: nil
end
object.enable_test_interface

View File

@ -29,7 +29,67 @@ RSpec.describe Dry::Configurable::DSL do
expect(setting.name).to be(:user)
expect(setting.value).to eql("root")
logger.rewind
expect(logger.string).to match(/#{FileUtils.pwd}.*default value as positional argument to settings is deprecated/)
expect(logger.string).to match(/default value as positional argument to settings is deprecated/)
end
it "compiles when giving a default as positional argument, and suppresses the warning when flagged off" do
Dry::Configurable.warn_on_setting_positional_default false
logger = StringIO.new
Dry::Core::Deprecations.set_logger!(logger)
setting = dsl.setting :user, "root"
expect(setting.name).to be(:user)
expect(setting.value).to eql("root")
logger.rewind
expect(logger.string).to be_empty
Dry::Configurable.warn_on_setting_positional_default true
end
it "compiles but deprecates giving a defalt hash value as a positional argument (without any keyword args)" do
# This test is necessary for behavior specific to Ruby 2.6 and 2.7
logger = StringIO.new
Dry::Core::Deprecations.set_logger!(logger)
setting = dsl.setting :default_options, {foo: "bar"}
expect(setting.name).to be(:default_options)
expect(setting.value).to eq(foo: "bar")
logger.rewind
expect(logger.string).to match(/default value as positional argument to settings is deprecated/)
if RUBY_VERSION < "3.0"
logger = StringIO.new
Dry::Core::Deprecations.set_logger!(logger)
setting = dsl.setting :default_options, foo: "bar"
expect(setting.name).to be(:default_options)
expect(setting.value).to eq(foo: "bar")
logger.rewind
expect(logger.string).to match(/default value as positional argument to settings is deprecated/)
end
end
it "compiles but deprecates giving a defalt hash value as a positional argument (with keyword args)" do
# This test is necessary for behavior specific to Ruby 2.6 and 2.7
logger = StringIO.new
Dry::Core::Deprecations.set_logger!(logger)
setting = dsl.setting :default_options, {foo: "bar"}, reader: true
expect(setting.name).to be(:default_options)
expect(setting.value).to eq(foo: "bar")
logger.rewind
expect(logger.string).to match(/default value as positional argument to settings is deprecated/)
end
it "does not infer a default hash value when non-valid keyword arguments are mixed in with valid keyword arguments" do
# This test is necessary for behavior specific to Ruby 2.6 and 2.7
expect { dsl.setting :default_options, foo: "bar", reader: true }.to raise_error ArgumentError, "Invalid options: [:foo]"
end
it "compiles a setting with a reader set" do
@ -71,7 +131,23 @@ RSpec.describe Dry::Configurable::DSL do
expect(setting.name).to be(:dsn)
expect(setting.value).to eql("jdbc:sqlite")
logger.rewind
expect(logger.string).to match(/#{FileUtils.pwd}.*constructor as a block is deprecated/)
expect(logger.string).to match(/constructor as a block is deprecated/)
end
it "supports but deprecates giving a constructor as a block, and suppresses the warning when flagged off" do
Dry::Configurable.warn_on_setting_constructor_block false
logger = StringIO.new
Dry::Core::Deprecations.set_logger!(logger)
setting = dsl.setting(:dsn, default: "sqlite") { |value| "jdbc:#{value}" }
expect(setting.name).to be(:dsn)
expect(setting.value).to eql("jdbc:sqlite")
logger.rewind
expect(logger.string).to be_empty
Dry::Configurable.warn_on_setting_constructor_block true
end
it "compiles a nested list of settings" do