dry-configurable 0.7.0 → 0.8.0

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
- 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