dry-configurable 0.7.0 → 0.8.0

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
- SHA1:
3
- metadata.gz: eeb18b428640615a653b1e669bcbedf1130f83ef
4
- data.tar.gz: 294ec402fe8dbfb378c6a5e1d5d442c49ac7baf1
2
+ SHA256:
3
+ metadata.gz: b54f274cf2ae374ac89bd1e3342da1ccf4c066ab220bdb8fd19c726be6f5d717
4
+ data.tar.gz: 6045cf60f22c40d231335ec10b84207fda52e888ca6eeba5d953267af782f148
5
5
  SHA512:
6
- metadata.gz: f3c268381efd9f202447a9785873ea151e5ce4edc004c87006a0c5b91165b3b29827d99799cf2f75a386214c5906b09d9b12983aec05502ad88591f1fa702896
7
- data.tar.gz: fd8c289e25a8fa28ec55e1f69ec5f569e2b6dbf0506ca7df49a80c6277c99510bc0505dc7c063fd97144f2723105bba62448472bc2db73c941dce1bca8db9cbf
6
+ metadata.gz: 02f95a505041d9b8d6ea199ce17aff5fd37e076577be2d5fd690a6350956f46c553a9d9972d447acdf6a97d370a44c6605c2c0c88f59ec821034977d9e33d6e1
7
+ data.tar.gz: af67f8149439b833d1898671ed187a5d1ca1e7d85aa6670723759016188092d8476b0a66574eedd70972c7f52f6534039a58854864c5e913f3d2f724696de9cc
data/.gitignore CHANGED
@@ -2,7 +2,6 @@
2
2
  coverage
3
3
  /.bundle
4
4
  vendor/bundle
5
- bin/
6
5
  tmp/
7
6
  .idea/
8
7
  Gemfile.lock
data/.travis.yml CHANGED
@@ -8,22 +8,15 @@ script:
8
8
  after_success:
9
9
  - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
10
10
  rvm:
11
- - 2.0
12
- - 2.1
13
- - 2.2
14
- - 2.3.1
15
- - rbx-3
16
- - jruby-9.1.5.0
17
- - ruby-head
18
- - jruby-head
11
+ - 2.3.8
12
+ - 2.4.5
13
+ - 2.5.3
14
+ - 2.6.0
15
+ - jruby-9.2.5.0
19
16
  env:
20
17
  global:
21
18
  - JRUBY_OPTS='--dev -J-Xmx1024M'
22
19
  - COVERAGE='true'
23
- matrix:
24
- allow_failures:
25
- - rvm: ruby-head
26
- - rvm: jruby-head
27
20
  notifications:
28
21
  email: false
29
22
  webhooks:
data/CHANGELOG.md CHANGED
@@ -1,4 +1,48 @@
1
- ## Unreleased
1
+ ## 0.8.0 - 2019-02-05
2
+
3
+ ## Fixed
4
+
5
+ * 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))
6
+
7
+ ## Added
8
+
9
+ * Support for instance-level configuration landed. For usage, `include` the module instead of extending ([flash-gordon](https://github.com/flash-gordon))
10
+ ```ruby
11
+ class App
12
+ include Dry::Configurable
13
+
14
+ setting :database
15
+ end
16
+
17
+ production = App.new
18
+ production.config.database = ENV['DATABASE_URL']
19
+ production.finalize!
20
+
21
+ development = App.new
22
+ development.config.database = 'jdbc:sqlite:memory'
23
+ development.finalize!
24
+ ```
25
+ * Config values can be set from a hash with `.update`. Nested settings are supported ([flash-gordon](https://github.com/flash-gordon))
26
+ ```ruby
27
+ class App
28
+ extend Dry::Configurable
29
+
30
+ setting :db do
31
+ setting :host
32
+ setting :port
33
+ end
34
+
35
+ config.update(YAML.load(File.read("config.yml")))
36
+ end
37
+ ```
38
+
39
+ ## Changed
40
+
41
+ * [BREAKING] Minimal supported Ruby version is set to 2.3 ([flash-gordon](https://github.com/flash-gordon))
42
+
43
+ [Compare v0.7.0...v0.8.0](https://github.com/dry-rb/dry-configurable/compare/v0.7.0...v0.8.0)
44
+
45
+ ## 0.7.0 - 2017-04-25
2
46
 
3
47
  ## Added
4
48
 
@@ -10,4 +54,4 @@
10
54
  * Convert nested configs to nested hashes with `Config#to_h` ([saverio-kantox](https://github.com/saverio-kantox))
11
55
  * Disallow modification on frozen config ([qcam](https://github.com/qcam))
12
56
 
13
- [Compare v0.6.2...HEAD](https://github.com/dry-rb/dry-configurable/compare/v0.6.2...HEAD)
57
+ [Compare v0.6.2...v0.7.0](https://github.com/dry-rb/dry-configurable/compare/v0.6.2...v0.7.0)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,29 @@
1
+ # Issue Guidelines
2
+
3
+ ## Reporting bugs
4
+
5
+ If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated.
6
+
7
+ ## Reporting feature requests
8
+
9
+ Report a feature request **only after discussing it first on [discuss.dry-rb.org](https://discuss.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
10
+
11
+ ## Reporting questions, support requests, ideas, concerns etc.
12
+
13
+ **PLEASE DON'T** - use [discuss.dry-rb.org](http://discuss.dry-rb.org) instead.
14
+
15
+ # Pull Request Guidelines
16
+
17
+ A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc.
18
+
19
+ Other requirements:
20
+
21
+ 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue.
22
+ 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
23
+ 3) Add API documentation if it's a new feature
24
+ 4) Update API documentation if it changes an existing feature
25
+ 5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](github.com/dry-rb/dry-rb.org) which updates user documentation and guides
26
+
27
+ # Asking for help
28
+
29
+ If these guidelines aren't helpful, and you're stuck, please post a message on [discuss.dry-rb.org](https://discuss.dry-rb.org).
data/Gemfile CHANGED
@@ -13,4 +13,5 @@ group :tools do
13
13
  gem 'guard'
14
14
  gem 'guard-rspec'
15
15
  gem 'listen', '3.0.6'
16
+ gem 'pry-byebug', platform: :mri
16
17
  end
data/README.md CHANGED
@@ -1,47 +1,34 @@
1
1
  [gitter]: https://gitter.im/dry-rb/chat
2
2
  [gem]: https://rubygems.org/gems/dry-configurable
3
3
  [travis]: https://travis-ci.org/dry-rb/dry-configurable
4
- [code_climate]: https://codeclimate.com/github/dry-rb/dry-configurable
5
4
  [inch]: http://inch-ci.org/github/dry-rb/dry-configurable
6
5
 
7
6
  # dry-configurable [![Join the Gitter chat](https://badges.gitter.im/Join%20Chat.svg)][gitter]
8
7
 
9
8
  [![Gem Version](https://img.shields.io/gem/v/dry-configurable.svg)][gem]
10
9
  [![Build Status](https://img.shields.io/travis/dry-rb/dry-configurable.svg)][travis]
11
- [![Code Climate](https://img.shields.io/codeclimate/github/dry-rb/dry-configurable.svg)][code_climate]
12
- [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/dry-rb/dry-configurable.svg)][code_climate]
10
+ [![Maintainability](https://api.codeclimate.com/v1/badges/25311e81391498d6b7c8/maintainability)](https://codeclimate.com/github/dry-rb/dry-configurable/maintainability)
11
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/25311e81391498d6b7c8/test_coverage)](https://codeclimate.com/github/dry-rb/dry-configurable/test_coverage)
13
12
  [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-configurable.svg)][inch]
14
13
 
15
- ## Synopsis
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
16
17
 
17
18
  ```ruby
18
- class App
19
- extend Dry::Configurable
20
-
21
- # Pass a block for nested configuration (works to any depth)
22
- setting :database do
23
- # Can pass a default value
24
- setting :dsn, 'sqlite:memory'
25
- end
26
- # Defaults to nil if no default value is given
27
- setting :adapter
28
- # Passing the reader option as true will create reader method for the class
29
- setting :pool, 5, reader: true
30
- # Passing the reader attributes works with nested configuration
31
- setting :uploader, reader: true do
32
- setting :bucket, 'dev'
33
- end
34
- end
35
-
36
- App.configure do |config|
37
- config.database.dsn = 'jdbc:sqlite:memory'
38
- end
39
-
40
- App.config.database.dsn
41
- # => 'jdbc:sqlite:memory'
42
- App.config.adapter # => nil
43
- App.pool # => 5
44
- App.uploader.bucket # => 'dev'
19
+ gem 'dry-configurable'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```sh
25
+ $ bundle
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```sh
31
+ $ gem install dry-configurable
45
32
  ```
46
33
 
47
34
  ## Links
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  require File.expand_path('../lib/dry/configurable/version', __FILE__)
3
2
 
4
3
  Gem::Specification.new do |spec|
@@ -7,15 +6,17 @@ Gem::Specification.new do |spec|
7
6
  spec.authors = ['Andy Holland']
8
7
  spec.email = ['andyholland1991@aol.com']
9
8
  spec.summary = 'A mixin to add configuration functionality to your classes'
10
- spec.homepage = 'https://github.com/dryrb/dry-configurable'
9
+ spec.homepage = 'https://github.com/dry-rb/dry-configurable'
11
10
  spec.license = 'MIT'
12
11
 
13
- spec.files = `git ls-files -z`.split("\x0")
14
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
12
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|bin)/}) }
13
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
15
14
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
15
  spec.require_paths = ['lib']
17
16
 
17
+ spec.required_ruby_version = ">= 2.3.0"
18
18
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
19
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.7'
19
20
 
20
21
  spec.add_development_dependency 'bundler'
21
22
  spec.add_development_dependency 'rake'
@@ -1,61 +1,87 @@
1
+ require 'concurrent/hash'
2
+
1
3
  module Dry
2
4
  module Configurable
3
5
  # @private
4
6
  class Config
5
- DEFAULT_PROCESSOR = ->(v) { v }.freeze
7
+ class << self
8
+ # @private
9
+ def [](settings)
10
+ ::Class.new(Config) do
11
+ @settings = settings
12
+ singleton_class.send(:attr_reader, :settings)
13
+
14
+ @lock = ::Mutex.new
15
+ @config_defined = false
16
+ end
17
+ end
6
18
 
7
- def self.create(settings)
8
- klass = ::Class.new(self)
19
+ # @private
20
+ def define_accessors!
21
+ @lock.synchronize do
22
+ break if config_defined?
9
23
 
10
- settings.each do |setting|
11
- klass.__send__(:define_method, setting.name) do
12
- @config[setting.name]
13
- end
24
+ settings.each do |setting|
25
+ define_method(setting.name) do
26
+ @config[setting.name]
27
+ end
14
28
 
15
- klass.__send__(:define_method, "#{setting.name}=") do |value|
16
- raise_frozen_config if frozen?
17
- @config[setting.name] = setting.processor.call(value)
29
+ define_method("#{setting.name}=") do |value|
30
+ raise FrozenConfig, 'Cannot modify frozen config' if frozen?
31
+ @config[setting.name] = setting.processor.(value)
32
+ end
33
+ end
34
+
35
+ @config_defined = true
18
36
  end
19
37
  end
20
38
 
21
- klass.new(settings)
39
+ # @private
40
+ def config_defined?
41
+ @config_defined
42
+ end
22
43
  end
23
44
 
24
- def initialize(settings)
45
+ def initialize
25
46
  @config = ::Concurrent::Hash.new
26
-
27
- settings.each do |setting|
28
- if setting.none?
29
- @config[setting.name] = nil
30
- else
31
- public_send("#{setting.name}=", setting.value)
32
- end
33
- end
47
+ @lock = ::Mutex.new
48
+ @defined = false
34
49
  end
35
50
 
36
- def dup
37
- dup = super
38
- dup.instance_variable_set(:@config, @config.dup)
39
- dup
51
+ def defined?
52
+ @defined
40
53
  end
41
54
 
42
- def clone
43
- clone = super
44
- clone.instance_variable_set(:@config, @config.clone)
45
- clone
55
+ # @private
56
+ def define!(parent_config = EMPTY_HASH)
57
+ @lock.synchronize do
58
+ break if self.defined?
59
+
60
+ self.class.define_accessors!
61
+ set_values!(parent_config)
62
+
63
+ @defined = true
64
+ end
65
+
66
+ self
46
67
  end
47
68
 
69
+ # @private
48
70
  def finalize!
71
+ define!
49
72
  @config.freeze
50
73
  freeze
51
74
  end
52
75
 
76
+ # Serialize config to a Hash
77
+ #
78
+ # @return [Hash]
79
+ #
80
+ # @api public
53
81
  def to_h
54
- @config.each_with_object({}) do |tuple, hash|
55
- key, value = tuple
56
-
82
+ @config.each_with_object({}) do |(key, value), hash|
57
83
  case value
58
- when ::Dry::Configurable::Config, ::Dry::Configurable::NestedConfig
84
+ when Config
59
85
  hash[key] = value.to_h
60
86
  else
61
87
  hash[key] = value
@@ -64,29 +90,72 @@ module Dry
64
90
  end
65
91
  alias to_hash to_h
66
92
 
93
+ # Get config value by a key
94
+ #
95
+ # @param [String,Symbol] name
96
+ #
97
+ # @return Config value
67
98
  def [](name)
68
- raise_unknown_setting_error(name) unless setting?(name)
99
+ raise_unknown_setting_error(name) unless key?(name.to_sym)
69
100
  public_send(name)
70
101
  end
71
102
 
103
+ # Set config value.
104
+ # Note that finalized configs cannot be changed.
105
+ #
106
+ # @param [String,Symbol] name
107
+ # @param [Object] value
72
108
  def []=(name, value)
73
- raise_unknown_setting_error(name) unless setting?(name)
109
+ raise_unknown_setting_error(name) unless key?(name.to_sym)
74
110
  public_send("#{name}=", value)
75
111
  end
76
112
 
113
+ # Whether config has a key
114
+ #
115
+ # @param [Symbol] key
116
+ # @return [Bool]
117
+ def key?(name)
118
+ self.class.settings.name?(name)
119
+ end
120
+
121
+ # Recursively update values from a hash
122
+ #
123
+ # @param [Hash] values to set
124
+ # @return [Config]
125
+ def update(values)
126
+ values.each do |key, value|
127
+ if self[key].is_a?(Config)
128
+ self[key].update(value)
129
+ else
130
+ self[key] = value
131
+ end
132
+ end
133
+ self
134
+ end
135
+
77
136
  private
78
137
 
79
- def raise_frozen_config
80
- raise FrozenConfig, 'Cannot modify frozen config'
138
+ # @private
139
+ def set_values!(parent)
140
+ self.class.settings.each do |setting|
141
+ if parent.key?(setting.name) && !setting.node?
142
+ @config[setting.name] = parent[setting.name]
143
+ elsif setting.undefined?
144
+ @config[setting.name] = nil
145
+ elsif setting.node?
146
+ value = setting.value.create_config
147
+ value.define!(parent.fetch(setting.name, EMPTY_HASH))
148
+ self[setting.name] = value
149
+ else
150
+ self[setting.name] = setting.value
151
+ end
152
+ end
81
153
  end
82
154
 
155
+ # @private
83
156
  def raise_unknown_setting_error(name)
84
157
  raise ArgumentError, "+#{name}+ is not a setting name"
85
158
  end
86
-
87
- def setting?(name)
88
- @config.key?(name.to_sym)
89
- end
90
159
  end
91
160
  end
92
161
  end
@@ -1,5 +1,3 @@
1
- # A collection of micro-libraries, each intended to encapsulate
2
- # a common task in Ruby
3
1
  module Dry
4
2
  module Configurable
5
3
  Error = Class.new(::StandardError)
@@ -0,0 +1,37 @@
1
+ module Dry
2
+ module Configurable
3
+ # This class represents a setting and is used internally.
4
+ #
5
+ # @private
6
+ class Setting
7
+ attr_reader :name
8
+
9
+ attr_reader :options
10
+
11
+ attr_reader :processor
12
+
13
+ def initialize(name, value, processor, options = EMPTY_HASH)
14
+ @name = name.to_sym
15
+ @value = value
16
+ @processor = processor
17
+ @options = options
18
+ end
19
+
20
+ def value
21
+ Undefined.default(@value, nil)
22
+ end
23
+
24
+ def undefined?
25
+ Undefined.equal?(@value)
26
+ end
27
+
28
+ def reader?
29
+ options[:reader]
30
+ end
31
+
32
+ def node?
33
+ Settings === @value
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ module Dry
2
+ # Argument parser
3
+ #
4
+ # Passing and array or arguments, it will decide which one are arguments
5
+ # and which one are options.
6
+ #
7
+ # We have a limitation if setting the value without options, as a hash
8
+ # having the same key as one of the valid options, will parse the value
9
+ # as options. In this case, all unknown options will be reject with an exception.
10
+ #
11
+ # @example
12
+ # p = Dry::Configurable::ArgumentParser.new.('db:sqlite', reader: true)
13
+ #
14
+ # p[0] # => 'db:sqlite'
15
+ # p[1] # => ArgumentParser::DEFAULT_PROCESSOR
16
+ # p[2] # => { reader: true }
17
+ module Configurable
18
+ class Settings
19
+ # @private
20
+ class ArgumentParser
21
+ DEFAULT_PROCESSOR = ->(v) { v }
22
+
23
+ # @private
24
+ def call(val, opts, block)
25
+ if block && block.parameters.empty?
26
+ raise ArgumentError unless Undefined.equal?(opts)
27
+
28
+ processor = DEFAULT_PROCESSOR
29
+
30
+ value, options = Settings.capture(&block), val
31
+ else
32
+ processor = block || DEFAULT_PROCESSOR
33
+
34
+ if Undefined.equal?(opts) && val.is_a?(Hash) && val.key?(:reader)
35
+ value, options = Undefined, val
36
+ else
37
+ value, options = val, opts
38
+ end
39
+ end
40
+
41
+ [value, processor, options(Undefined.default(options, EMPTY_HASH))]
42
+ end
43
+
44
+ def options(reader: false)
45
+ { reader: reader }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,92 @@
1
+ require 'set'
2
+ require 'concurrent/array'
3
+ require 'dry/configurable/settings/argument_parser'
4
+ require 'dry/configurable/setting'
5
+ require 'dry/configurable/config'
6
+
7
+ module Dry
8
+ module Configurable
9
+ # A collection of settings. This is not part of the public API.
10
+ #
11
+ # @private
12
+ class Settings
13
+ Parser = ArgumentParser.new.freeze
14
+
15
+ class DSL
16
+ def self.call(&block)
17
+ new.instance_exec do
18
+ instance_exec(&block)
19
+ @settings
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ @settings = Settings.new
25
+ end
26
+
27
+ def setting(*args, &block)
28
+ @settings.add(*args, &block)
29
+ end
30
+ end
31
+
32
+ # Capture nested config definition
33
+ #
34
+ # @return [Dry::Configurable::Setting]
35
+ def self.capture(&block)
36
+ DSL.(&block)
37
+ end
38
+
39
+ attr_reader :settings
40
+
41
+ attr_reader :config_class
42
+
43
+ attr_reader :names
44
+
45
+ def initialize(settings = ::Concurrent::Array.new)
46
+ @settings = settings
47
+ @config_class = Config[self]
48
+ @names = Set.new(settings.map(&:name))
49
+ yield(self) if block_given?
50
+ end
51
+
52
+ def add(key, value = Undefined, options = Undefined, &block)
53
+ extended = singleton_class < Configurable
54
+ raise_already_defined_config(key) if extended && configured?
55
+
56
+ Setting.new(key, *Parser.(value, options, block)).tap do |s|
57
+ settings << s
58
+ names << s.name
59
+ end
60
+ end
61
+
62
+ def each
63
+ settings.each { |s| yield(s) }
64
+ end
65
+
66
+ def empty?
67
+ settings.empty?
68
+ end
69
+
70
+ def name?(name)
71
+ names.include?(name)
72
+ end
73
+
74
+ def dup
75
+ Settings.new(settings.dup)
76
+ end
77
+
78
+ def freeze
79
+ settings.freeze
80
+ super
81
+ end
82
+
83
+ def create_config
84
+ config_class.new
85
+ end
86
+
87
+ def config_defined?
88
+ config_class.config_defined?
89
+ end
90
+ end
91
+ end
92
+ end
@@ -8,7 +8,11 @@ module Dry
8
8
  #
9
9
  # @api public
10
10
  def reset_config
11
- create_config
11
+ if self.is_a?(Class)
12
+ @config = _settings.create_config
13
+ else
14
+ @config = self.class._settings.create_config
15
+ end
12
16
  end
13
17
  end
14
18
 
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  module Configurable
3
3
  # @api public
4
- VERSION = '0.7.0'.freeze
4
+ VERSION = '0.8.0'.freeze
5
5
  end
6
6
  end