dry-configurable 0.1.7 → 0.7.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
2
  SHA1:
3
- metadata.gz: 58a630055aefd53b2ec5d0261f353e9779dcab8f
4
- data.tar.gz: 42b5d3dddc4d819a6e80adcd3d9b341fed1cbea0
3
+ metadata.gz: eeb18b428640615a653b1e669bcbedf1130f83ef
4
+ data.tar.gz: 294ec402fe8dbfb378c6a5e1d5d442c49ac7baf1
5
5
  SHA512:
6
- metadata.gz: d13cd68936ab7a5f96ed705011b1bd5cfb8112091a825fa2fb38e3afe4fdfd72be2f2646504dd00c543bc95ef310e1bb67b1a3d129c683a0d73b63ab3d31a0f0
7
- data.tar.gz: 5ac518f3fc7f260431a9b5122045c6a53b32929529c2384618ef870fc2da399e9ffa4cc66d20239258a7422c14501b16431fcbe0aac26e9bba940bd0bf63d77f
6
+ metadata.gz: f3c268381efd9f202447a9785873ea151e5ce4edc004c87006a0c5b91165b3b29827d99799cf2f75a386214c5906b09d9b12983aec05502ad88591f1fa702896
7
+ data.tar.gz: fd8c289e25a8fa28ec55e1f69ec5f569e2b6dbf0506ca7df49a80c6277c99510bc0505dc7c063fd97144f2723105bba62448472bc2db73c941dce1bca8db9cbf
data/.gitignore CHANGED
@@ -6,3 +6,4 @@ bin/
6
6
  tmp/
7
7
  .idea/
8
8
  Gemfile.lock
9
+ spec/examples.txt
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
1
  --color
2
- --require spec_helper
3
- --order random
2
+ --require ./spec/spec_helper.rb
data/.travis.yml CHANGED
@@ -1,20 +1,25 @@
1
1
  language: ruby
2
- sudo: false
2
+ dist: trusty
3
+ sudo: required
3
4
  cache: bundler
4
5
  bundler_args: --without console
5
6
  script:
6
7
  - bundle exec rake spec
8
+ after_success:
9
+ - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
7
10
  rvm:
8
11
  - 2.0
9
12
  - 2.1
10
13
  - 2.2
11
- - rbx-2
12
- - jruby
14
+ - 2.3.1
15
+ - rbx-3
16
+ - jruby-9.1.5.0
13
17
  - ruby-head
14
18
  - jruby-head
15
19
  env:
16
20
  global:
17
21
  - JRUBY_OPTS='--dev -J-Xmx1024M'
22
+ - COVERAGE='true'
18
23
  matrix:
19
24
  allow_failures:
20
25
  - rvm: ruby-head
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ ## Unreleased
2
+
3
+ ## Added
4
+
5
+ * Introduce `Configurable.finalize!` which freezes config and its dependencies ([qcam](https://github.com/qcam))
6
+
7
+ ## Fixed
8
+
9
+ * Allow for boolean false as default setting value ([yuszuv](https://github.com/yuszuv))
10
+ * Convert nested configs to nested hashes with `Config#to_h` ([saverio-kantox](https://github.com/saverio-kantox))
11
+ * Disallow modification on frozen config ([qcam](https://github.com/qcam))
12
+
13
+ [Compare v0.6.2...HEAD](https://github.com/dry-rb/dry-configurable/compare/v0.6.2...HEAD)
data/Gemfile CHANGED
@@ -3,13 +3,14 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem 'codeclimate-test-reporter', platform: :rbx
6
+ platforms :mri do
7
+ gem 'codeclimate-test-reporter', require: false
8
+ gem 'simplecov', require: false
9
+ end
7
10
  end
8
11
 
9
12
  group :tools do
10
- gem 'rubocop'
11
13
  gem 'guard'
12
14
  gem 'guard-rspec'
13
- gem 'guard-rubocop'
14
15
  gem 'listen', '3.0.6'
15
16
  end
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014 Ruby Object Mapper
3
+ Copyright (c) 2015-2017 dry-rb
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
@@ -25,6 +25,12 @@ class App
25
25
  end
26
26
  # Defaults to nil if no default value is given
27
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
28
34
  end
29
35
 
30
36
  App.configure do |config|
@@ -34,8 +40,14 @@ end
34
40
  App.config.database.dsn
35
41
  # => 'jdbc:sqlite:memory'
36
42
  App.config.adapter # => nil
43
+ App.pool # => 5
44
+ App.uploader.bucket # => 'dev'
37
45
  ```
38
46
 
47
+ ## Links
48
+
49
+ * [Documentation](http://dry-rb.org/gems/dry-configurable)
50
+
39
51
  ## License
40
52
 
41
53
  See `LICENSE` file.
@@ -0,0 +1,91 @@
1
+ module Dry
2
+ # Argument parser
3
+ #
4
+ # Passing and array or arguments, it will decide wich 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.
10
+ #
11
+ # @example
12
+ # p = Dry::Configurable::ArgumentParser.new(['db:sqlite', { reader: true })
13
+ #
14
+ # p.value # => 'db:sqlite'
15
+ # p.options # => { reader: true }
16
+ #
17
+ # Dry::Configurable::ArgumentParser.call(['db:sqlite', { reader: true })
18
+ # # => [ 'db:sqlite', { reader: true } ]
19
+ module Configurable
20
+ # @private
21
+ class ArgumentParser
22
+ VALID_OPTIONS = %i(reader).freeze
23
+
24
+ def self.call(data)
25
+ parsed = new(data)
26
+ [parsed.value, parsed.options]
27
+ end
28
+
29
+ def initialize(data)
30
+ @data = data
31
+ end
32
+
33
+ def value
34
+ parse_args[:value]
35
+ end
36
+
37
+ def options
38
+ parse_args[:options]
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :data
44
+
45
+ # @private
46
+ def default_args
47
+ { value: nil, options: {} }
48
+ end
49
+
50
+ # @private
51
+ def parse_args
52
+ return default_args if data.empty?
53
+ if data.size > 1
54
+ { value: data.first, options: check_options(data.last) }
55
+ else
56
+ default_args.merge(check_for_value_or_options(data.first))
57
+ end
58
+ end
59
+
60
+ # @private
61
+ def check_options(opts)
62
+ return {} if opts.empty?
63
+ opts.select { |k, _| VALID_OPTIONS.include?(k) }
64
+ end
65
+
66
+ # @private
67
+ def check_for_value_or_options(args)
68
+ case args
69
+ when Hash
70
+ parse_hash(args)
71
+ else
72
+ { value: args }
73
+ end
74
+ end
75
+
76
+ # @private
77
+ def parse_hash(args)
78
+ if hash_include_options_key(args)
79
+ { options: check_options(args) }
80
+ else
81
+ { value: args }
82
+ end
83
+ end
84
+
85
+ # @private
86
+ def hash_include_options_key(hash)
87
+ hash.any? { |k, _| VALID_OPTIONS.include?(k) }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,27 @@
1
+ module Dry
2
+ module Configurable
3
+ class Config
4
+ # @private
5
+ class Value
6
+ # @private
7
+ NONE = ::Object.new.freeze
8
+
9
+ attr_reader :name, :processor
10
+
11
+ def initialize(name, value, processor)
12
+ @name = name.to_sym
13
+ @value = value
14
+ @processor = processor
15
+ end
16
+
17
+ def value
18
+ none? ? nil : @value
19
+ end
20
+
21
+ def none?
22
+ @value.equal?(::Dry::Configurable::Config::Value::NONE)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,92 @@
1
1
  module Dry
2
2
  module Configurable
3
3
  # @private
4
- class Config < ::Struct
4
+ class Config
5
+ DEFAULT_PROCESSOR = ->(v) { v }.freeze
6
+
7
+ def self.create(settings)
8
+ klass = ::Class.new(self)
9
+
10
+ settings.each do |setting|
11
+ klass.__send__(:define_method, setting.name) do
12
+ @config[setting.name]
13
+ end
14
+
15
+ klass.__send__(:define_method, "#{setting.name}=") do |value|
16
+ raise_frozen_config if frozen?
17
+ @config[setting.name] = setting.processor.call(value)
18
+ end
19
+ end
20
+
21
+ klass.new(settings)
22
+ end
23
+
24
+ def initialize(settings)
25
+ @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
34
+ end
35
+
36
+ def dup
37
+ dup = super
38
+ dup.instance_variable_set(:@config, @config.dup)
39
+ dup
40
+ end
41
+
42
+ def clone
43
+ clone = super
44
+ clone.instance_variable_set(:@config, @config.clone)
45
+ clone
46
+ end
47
+
48
+ def finalize!
49
+ @config.freeze
50
+ freeze
51
+ end
52
+
53
+ def to_h
54
+ @config.each_with_object({}) do |tuple, hash|
55
+ key, value = tuple
56
+
57
+ case value
58
+ when ::Dry::Configurable::Config, ::Dry::Configurable::NestedConfig
59
+ hash[key] = value.to_h
60
+ else
61
+ hash[key] = value
62
+ end
63
+ end
64
+ end
65
+ alias to_hash to_h
66
+
67
+ def [](name)
68
+ raise_unknown_setting_error(name) unless setting?(name)
69
+ public_send(name)
70
+ end
71
+
72
+ def []=(name, value)
73
+ raise_unknown_setting_error(name) unless setting?(name)
74
+ public_send("#{name}=", value)
75
+ end
76
+
77
+ private
78
+
79
+ def raise_frozen_config
80
+ raise FrozenConfig, 'Cannot modify frozen config'
81
+ end
82
+
83
+ def raise_unknown_setting_error(name)
84
+ raise ArgumentError, "+#{name}+ is not a setting name"
85
+ end
86
+
87
+ def setting?(name)
88
+ @config.key?(name.to_sym)
89
+ end
5
90
  end
6
91
  end
7
92
  end
@@ -0,0 +1,9 @@
1
+ # A collection of micro-libraries, each intended to encapsulate
2
+ # a common task in Ruby
3
+ module Dry
4
+ module Configurable
5
+ Error = Class.new(::StandardError)
6
+ AlreadyDefinedConfig = ::Class.new(Error)
7
+ FrozenConfig = ::Class.new(Error)
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ module Dry
2
+ module Configurable
3
+ # @private
4
+ class NestedConfig
5
+ def initialize(&block)
6
+ klass = ::Class.new { extend ::Dry::Configurable }
7
+ klass.instance_eval(&block)
8
+ @klass = klass
9
+ end
10
+
11
+ # @private no, really...
12
+ def create_config
13
+ if @klass.instance_variables.include?(:@_config)
14
+ @klass.__send__(:create_config)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def config
21
+ @klass.config
22
+ end
23
+
24
+ def method_missing(method, *args, &block)
25
+ config.respond_to?(method) ? config.public_send(method, *args, &block) : super
26
+ end
27
+
28
+ def respond_to_missing?(method, _include_private = false)
29
+ config.respond_to?(method) || super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Dry
2
+ module Configurable
3
+ # Methods meant to be used in a testing scenario
4
+ module TestInterface
5
+ # Resets configuration to default values
6
+ #
7
+ # @return [Dry::Configurable::Config]
8
+ #
9
+ # @api public
10
+ def reset_config
11
+ create_config
12
+ end
13
+ end
14
+
15
+ # Mixes in test interface into the configurable module
16
+ #
17
+ # @api public
18
+ def enable_test_interface
19
+ extend Dry::Configurable::TestInterface
20
+ end
21
+ end
22
+ end
@@ -1,6 +1,6 @@
1
1
  module Dry
2
2
  module Configurable
3
3
  # @api public
4
- VERSION = '0.1.7'.freeze
4
+ VERSION = '0.7.0'.freeze
5
5
  end
6
6
  end
@@ -1,5 +1,9 @@
1
1
  require 'concurrent'
2
2
  require 'dry/configurable/config'
3
+ require 'dry/configurable/error'
4
+ require 'dry/configurable/nested_config'
5
+ require 'dry/configurable/argument_parser'
6
+ require 'dry/configurable/config/value'
3
7
  require 'dry/configurable/version'
4
8
 
5
9
  # A collection of micro-libraries, each intended to encapsulate
@@ -29,15 +33,17 @@ module Dry
29
33
  # @private
30
34
  def self.extended(base)
31
35
  base.class_eval do
32
- @_config_mutex = Mutex.new
33
- @_settings = Concurrent::Map.new
36
+ @_config_mutex = ::Mutex.new
37
+ @_settings = ::Concurrent::Array.new
38
+ @_reader_attributes = ::Concurrent::Array.new
34
39
  end
35
40
  end
36
41
 
37
42
  # @private
38
43
  def inherited(subclass)
39
- subclass.instance_variable_set(:@_config_mutex, Mutex.new)
44
+ subclass.instance_variable_set(:@_config_mutex, ::Mutex.new)
40
45
  subclass.instance_variable_set(:@_settings, @_settings.clone)
46
+ subclass.instance_variable_set(:@_reader_attributes, @_reader_attributes.clone)
41
47
  subclass.instance_variable_set(:@_config, @_config.clone) if defined?(@_config)
42
48
  super
43
49
  end
@@ -49,9 +55,7 @@ module Dry
49
55
  # @api public
50
56
  def config
51
57
  return @_config if defined?(@_config)
52
- @_config_mutex.synchronize do
53
- @_config ||= Config.new(*_settings.keys).new(*_settings.values) unless _settings.empty?
54
- end
58
+ create_config
55
59
  end
56
60
 
57
61
  # Return configuration
@@ -62,9 +66,20 @@ module Dry
62
66
  #
63
67
  # @api public
64
68
  def configure
69
+ raise_frozen_config if frozen?
65
70
  yield(config) if block_given?
66
71
  end
67
72
 
73
+ # Finalize and freeze configuration
74
+ #
75
+ # @return [Dry::Configurable::Config]
76
+ #
77
+ # @api public
78
+ def finalize!
79
+ freeze
80
+ config.finalize!
81
+ end
82
+
68
83
  # Add a setting to the configuration
69
84
  #
70
85
  # @param [Mixed] key
@@ -79,9 +94,32 @@ module Dry
79
94
  # @return [Dry::Configurable::Config]
80
95
  #
81
96
  # @api public
82
- def setting(key, default = nil, &block)
83
- default = _config_for(&block) if block_given?
84
- _settings[key] = default
97
+ def setting(key, *args, &block)
98
+ raise_already_defined_config(key) if defined?(@_config)
99
+ value, options = ArgumentParser.call(args)
100
+ if block
101
+ if block.parameters.empty?
102
+ value = _config_for(&block)
103
+ else
104
+ processor = block
105
+ end
106
+ end
107
+
108
+ _settings << ::Dry::Configurable::Config::Value.new(
109
+ key,
110
+ !value.nil? ? value : ::Dry::Configurable::Config::Value::NONE,
111
+ processor || ::Dry::Configurable::Config::DEFAULT_PROCESSOR
112
+ )
113
+ store_reader_options(key, options) if options.any?
114
+ end
115
+
116
+ # Return an array of setting names
117
+ #
118
+ # @return [Array]
119
+ #
120
+ # @api public
121
+ def settings
122
+ _settings.map(&:name)
85
123
  end
86
124
 
87
125
  # @private no, really...
@@ -89,13 +127,59 @@ module Dry
89
127
  @_settings
90
128
  end
91
129
 
130
+ def _reader_attributes
131
+ @_reader_attributes
132
+ end
133
+
92
134
  private
93
135
 
94
136
  # @private
95
137
  def _config_for(&block)
96
- config_klass = Class.new { extend Dry::Configurable }
97
- config_klass.instance_eval(&block)
98
- config_klass.config
138
+ ::Dry::Configurable::NestedConfig.new(&block)
139
+ end
140
+
141
+ # @private
142
+ def create_config
143
+ @_config_mutex.synchronize do
144
+ create_config_for_nested_configurations
145
+ @_config = ::Dry::Configurable::Config.create(_settings) unless _settings.empty?
146
+ end
147
+ end
148
+
149
+ # @private
150
+ def create_config_for_nested_configurations
151
+ nested_configs.map(&:create_config)
152
+ end
153
+
154
+ # @private
155
+ def nested_configs
156
+ _settings.select { |setting| setting.value.is_a?(::Dry::Configurable::NestedConfig) }.map(&:value)
157
+ end
158
+
159
+ # @private
160
+ def raise_already_defined_config(key)
161
+ raise AlreadyDefinedConfig,
162
+ "Cannot add setting +#{key}+, #{self} is already configured"
163
+ end
164
+
165
+ # @private
166
+ def raise_frozen_config
167
+ raise FrozenConfig, 'Cannot modify frozen config'
168
+ end
169
+
170
+ # @private
171
+ def store_reader_options(key, options)
172
+ _reader_attributes << key if options.fetch(:reader, false)
173
+ end
174
+
175
+ # @private
176
+ def method_missing(method, *args, &block)
177
+ _reader_attributes.include?(method) ? config.public_send(method, *args, &block) : super
178
+ end
179
+
180
+ # @private
181
+ def respond_to_missing?(method, _include_private = false)
182
+ _reader_attributes.include?(method) || super
99
183
  end
100
184
  end
101
185
  end
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,92 @@
1
- # encoding: utf-8
1
+ if ENV['COVERAGE'] == 'true' && RUBY_ENGINE == 'ruby' && RUBY_VERSION == '2.3.1'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start do
5
+ add_filter '/spec/'
6
+ end
7
+ end
8
+
9
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
10
+ RSpec.configure do |config|
11
+ config.expect_with :rspec do |expectations|
12
+ # This option will default to `true` in RSpec 4. It makes the `description`
13
+ # and `failure_message` of custom matchers include text for helper methods
14
+ # defined using `chain`, e.g.:
15
+ # be_bigger_than(2).and_smaller_than(4).description
16
+ # # => "be bigger than 2 and smaller than 4"
17
+ # ...rather than:
18
+ # # => "be bigger than 2"
19
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
20
+ end
21
+
22
+ config.mock_with :rspec do |mocks|
23
+ # Prevents you from mocking or stubbing a method that does not exist on
24
+ # a real object. This is generally recommended, and will default to
25
+ # `true` in RSpec 4.
26
+ mocks.verify_partial_doubles = true
27
+ end
28
+
29
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
30
+ # have no way to turn it off -- the option exists only for backwards
31
+ # compatibility in RSpec 3). It causes shared context metadata to be
32
+ # inherited by the metadata hash of host groups and examples, rather than
33
+ # triggering implicit auto-inclusion in groups with matching metadata.
34
+ config.shared_context_metadata_behavior = :apply_to_host_groups
35
+
36
+ # This allows you to limit a spec run to individual examples or groups
37
+ # you care about by tagging them with `:focus` metadata. When nothing
38
+ # is tagged with `:focus`, all examples get run. RSpec also provides
39
+ # aliases for `it`, `describe`, and `context` that include `:focus`
40
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
41
+ config.filter_run_when_matching :focus
42
+
43
+ # Allows RSpec to persist some state between runs in order to support
44
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
45
+ # you configure your source control system to ignore this file.
46
+ config.example_status_persistence_file_path = 'spec/examples.txt'
47
+
48
+ # Limits the available syntax to the non-monkey patched syntax that is
49
+ # recommended. For more details, see:
50
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
51
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
52
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
53
+ config.disable_monkey_patching!
54
+
55
+ # This setting enables warnings. It's recommended, but in some cases may
56
+ # be too noisy due to issues in dependencies.
57
+ config.warnings = true
58
+
59
+ # Many RSpec users commonly either run the entire suite or an individual
60
+ # file, and it's useful to allow more verbose output when running an
61
+ # individual spec file.
62
+ if config.files_to_run.one?
63
+ # Use the documentation formatter for detailed output,
64
+ # unless a formatter has already been configured
65
+ # (e.g. via a command-line flag).
66
+ config.default_formatter = 'doc'
67
+ end
68
+
69
+ # Print the n slowest examples and example groups at the
70
+ # end of the spec run, to help surface which specs are running
71
+ # particularly slow.
72
+ config.profile_examples = 3
73
+
74
+ # Run specs in random order to surface order dependencies. If you find an
75
+ # order dependency and want to debug it, you can fix the order by providing
76
+ # the seed, which is printed after each run.
77
+ # --seed 1234
78
+ config.order = :random
79
+
80
+ # Seed global randomization in this process using the `--seed` CLI option.
81
+ # Setting this allows you to use `--seed` to deterministically reproduce
82
+ # test failures related to randomization by passing the same `--seed` value
83
+ # as the one that triggered the failure.
84
+ Kernel.srand config.seed
85
+ end
2
86
 
3
87
  require 'dry/configurable'
88
+ require 'dry/configurable/test_interface'
4
89
 
5
90
  Dir[Pathname(__FILE__).dirname.join('support/**/*.rb').to_s].each do |file|
6
91
  require file
7
92
  end
8
-
9
- if RUBY_ENGINE == 'rbx'
10
- require 'codeclimate-test-reporter'
11
- CodeClimate::TestReporter.start
12
- end