dry-configurable 0.9.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.
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ platforms :mri do
7
+ gem 'codeclimate-test-reporter', require: false
8
+ gem 'simplecov', require: false
9
+ end
10
+
11
+ gem 'warning'
12
+ end
13
+
14
+ group :tools do
15
+ gem 'guard'
16
+ gem 'guard-rspec'
17
+ gem 'listen', '3.0.6'
18
+ gem 'pry-byebug', platform: :mri
19
+ gem "ossy", git: "https://github.com/solnic/ossy.git", branch: "master"
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2019 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ [gitter]: https://gitter.im/dry-rb/chat
2
+ [gem]: https://rubygems.org/gems/dry-configurable
3
+ [travis]: https://travis-ci.org/dry-rb/dry-configurable
4
+ [inch]: http://inch-ci.org/github/dry-rb/dry-configurable
5
+ [chat]: https://dry-rb.zulipchat.com
6
+
7
+ # dry-configurable [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
+
9
+ [![Gem Version](https://img.shields.io/gem/v/dry-configurable.svg)][gem]
10
+ [![Build Status](https://img.shields.io/travis/dry-rb/dry-configurable.svg)][travis]
11
+ [![Maintainability](https://api.codeclimate.com/v1/badges/25311e81391498d6b7c8/maintainability)](https://codeclimate.com/github/dry-rb/dry-configurable/maintainability)
12
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/25311e81391498d6b7c8/test_coverage)](https://codeclimate.com/github/dry-rb/dry-configurable/test_coverage)
13
+ [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-configurable.svg)][inch]
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'dry-configurable'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```sh
26
+ $ bundle
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```sh
32
+ $ gem install dry-configurable
33
+ ```
34
+
35
+ ## Links
36
+
37
+ * [Documentation](http://dry-rb.org/gems/dry-configurable)
38
+
39
+ ## License
40
+
41
+ See `LICENSE` file.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
5
+
6
+ require 'rspec/core'
7
+ require 'rspec/core/rake_task'
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run all specs in spec directory'
12
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Introduction & Usage
3
+ description: Thread-safe configuration mixin
4
+ layout: gem-single
5
+ order: 7
6
+ type: gem
7
+ name: dry-configurable
8
+ sections:
9
+ - testing
10
+ ---
11
+
12
+ ### Introduction
13
+
14
+ `dry-configurable` is a simple mixin to add thread-safe configuration behaviour to your classes. There are many libraries that make use of configuration, and each seemed to have their own implementation with a similar or duplicate interface, so we thought it was strange that this behaviour had not already been encapsulated into a reusable gem, hence `dry-configurable` was born.
15
+
16
+ ### Usage
17
+
18
+ `dry-configurable` is extremely simple to use, just extend the mixin and use the `setting` macro to add configuration options:
19
+
20
+ ```ruby
21
+ class App
22
+ extend Dry::Configurable
23
+
24
+ # Pass a block for nested configuration (works to any depth)
25
+ setting :database do
26
+ # Can pass a default value
27
+ setting :dsn, 'sqlite:memory'
28
+ end
29
+ # Defaults to nil if no default value is given
30
+ setting :adapter
31
+ # Pre-process values
32
+ setting(:path, 'test') { |value| Pathname(value) }
33
+ # Passing the reader option as true will create attr_reader method for the class
34
+ setting :pool, 5, reader: true
35
+ # Passing the reader attributes works with nested configuration
36
+ setting :uploader, reader: true do
37
+ setting :bucket, 'dev'
38
+ end
39
+ end
40
+
41
+ App.config.database.dsn
42
+ # => "sqlite:memory"
43
+
44
+ App.config.database.dsn = 'jdbc:sqlite:memory'
45
+ App.config.database.dsn
46
+ # => "jdbc:sqlite:memory"
47
+ App.config.adapter
48
+ # => nil
49
+ App.config.path
50
+ # => #<Pathname:test>
51
+ App.pool
52
+ # => 5
53
+ App.uploader.bucket
54
+ # => 'dev'
55
+ ```
@@ -0,0 +1,27 @@
1
+ ---
2
+ title: Testing
3
+ layout: gem-single
4
+ name: dry-configurable
5
+ ---
6
+
7
+ ### How to reset the config to its original state on testing environment
8
+
9
+ update `spec_helper.rb` :
10
+
11
+ ```ruby
12
+ require "dry/configurable/test_interface"
13
+
14
+ # this is your module/class that extended by Dry::Configurable
15
+ module AwesomeModule
16
+ enable_test_interface
17
+ end
18
+ ```
19
+
20
+ and on spec file (`xxx_spec.rb`) :
21
+
22
+ ```ruby
23
+ before(:all) { AwesomeModule.reset_config }
24
+ # or
25
+ before(:each) { AwesomeModule.reset_config }
26
+
27
+ ```
@@ -0,0 +1,29 @@
1
+ require File.expand_path('../lib/dry/configurable/version', __FILE__)
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'dry-configurable'
5
+ spec.version = Dry::Configurable::VERSION
6
+ spec.authors = ['Andy Holland']
7
+ spec.email = ['andyholland1991@aol.com']
8
+ spec.summary = 'A mixin to add configuration functionality to your classes'
9
+ spec.homepage = 'https://github.com/dry-rb/dry-configurable'
10
+ spec.license = 'MIT'
11
+
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) }
14
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
+ spec.require_paths = ['lib']
16
+
17
+ spec.metadata = {
18
+ 'source_code_uri' => 'https://github.com/dry-rb/dry-configurable',
19
+ 'changelog_uri' => 'https://github.com/dry-rb/dry-configurable/blob/master/CHANGELOG.md'
20
+ }
21
+
22
+ spec.required_ruby_version = ">= 2.4.0"
23
+ spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
24
+ spec.add_runtime_dependency 'dry-core', '~> 0.4', '>= 0.4.7'
25
+
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'rspec'
29
+ end
@@ -0,0 +1 @@
1
+ require 'dry/configurable'
@@ -0,0 +1,191 @@
1
+ require 'dry/core/constants'
2
+ require 'dry/configurable/settings'
3
+ require 'dry/configurable/error'
4
+ require 'dry/configurable/version'
5
+
6
+ # A collection of micro-libraries, each intended to encapsulate
7
+ # a common task in Ruby
8
+ module Dry
9
+ # A simple configuration mixin
10
+ #
11
+ # @example class-level configuration
12
+ #
13
+ # class App
14
+ # extend Dry::Configurable
15
+ #
16
+ # setting :database do
17
+ # setting :dsn, 'sqlite:memory'
18
+ # end
19
+ # end
20
+ #
21
+ # App.config.database.dsn = 'jdbc:sqlite:memory'
22
+ # App.config.database.dsn
23
+ # # => "jdbc:sqlite:memory"
24
+ #
25
+ # @example instance-level configuration
26
+ #
27
+ # class App
28
+ # include Dry::Configurable
29
+ #
30
+ # setting :database
31
+ # end
32
+ #
33
+ # production = App.new
34
+ # production.config.database = ENV['DATABASE_URL']
35
+ # production.finalize!
36
+ #
37
+ # development = App.new
38
+ # development.config.database = 'jdbc:sqlite:memory'
39
+ # development.finalize!
40
+ #
41
+ # @api public
42
+ module Configurable
43
+ include Dry::Core::Constants
44
+
45
+ module ClassMethods
46
+ # @private
47
+ def self.extended(base)
48
+ base.instance_exec do
49
+ @settings = Settings.new
50
+ end
51
+ end
52
+
53
+ # Add a setting to the configuration
54
+ #
55
+ # @param [Mixed] key
56
+ # The accessor key for the configuration value
57
+ # @param [Mixed] default
58
+ # The default config value
59
+ #
60
+ # @yield
61
+ # If a block is given, it will be evaluated in the context of
62
+ # a new configuration class, and bound as the default value
63
+ #
64
+ # @return [Dry::Configurable::Config]
65
+ #
66
+ # @api public
67
+ def setting(key, value = Undefined, options = Undefined, &block)
68
+ raise_already_defined_config(key) if _settings.config_defined?
69
+
70
+ setting = _settings.add(key, value, options, &block)
71
+
72
+ if setting.reader?
73
+ readers = singleton_class < Configurable ? singleton_class : self
74
+ readers.send(:define_method, setting.name) { config[setting.name] }
75
+ end
76
+ end
77
+
78
+ # Return an array of setting names
79
+ #
80
+ # @return [Set]
81
+ #
82
+ # @api public
83
+ def settings
84
+ _settings.names
85
+ end
86
+
87
+ # @private no, really...
88
+ def _settings
89
+ @settings
90
+ end
91
+
92
+ private
93
+
94
+ # @private
95
+ def raise_already_defined_config(key)
96
+ raise AlreadyDefinedConfig,
97
+ "Cannot add setting +#{key}+, #{self} is already configured"
98
+ end
99
+
100
+ # @private
101
+ def inherited(subclass)
102
+ parent = self
103
+ subclass.instance_exec do
104
+ @settings = parent._settings.dup
105
+ end
106
+
107
+ if singleton_class < Configurable
108
+ parent_config = @config
109
+ subclass.instance_exec do
110
+ @config = _settings.create_config
111
+ @config.define!(parent_config.to_h) if parent_config.defined?
112
+ end
113
+ end
114
+
115
+ super
116
+ end
117
+ end
118
+
119
+ class << self
120
+ # @private
121
+ def extended(base)
122
+ base.extend(ClassMethods)
123
+ base.class_eval do
124
+ @config = _settings.create_config
125
+ end
126
+ end
127
+
128
+ # @private
129
+ def included(base)
130
+ base.extend(ClassMethods)
131
+ end
132
+ end
133
+
134
+ # @private
135
+ def initialize(*)
136
+ @config = self.class._settings.create_config
137
+ super
138
+ end
139
+
140
+ # Return configuration
141
+ #
142
+ # @return [Dry::Configurable::Config]
143
+ #
144
+ # @api public
145
+ def config
146
+ return @config if @config.defined?
147
+ @config.define!
148
+ end
149
+
150
+ # Return configuration
151
+ #
152
+ # @yield [Dry::Configuration::Config]
153
+ #
154
+ # @return [Dry::Configurable::Config]
155
+ #
156
+ # @api public
157
+ def configure
158
+ raise FrozenConfig, 'Cannot modify frozen config' if frozen?
159
+ yield(config) if block_given?
160
+ self
161
+ end
162
+
163
+ # Finalize and freeze configuration
164
+ #
165
+ # @return [Dry::Configurable::Config]
166
+ #
167
+ # @api public
168
+ def finalize!
169
+ freeze
170
+ config.finalize!
171
+ end
172
+
173
+ # @api public
174
+ def dup
175
+ super.tap do |copy|
176
+ copy.instance_variable_set(:@config, config.dup)
177
+ end
178
+ end
179
+
180
+ # @api public
181
+ def clone
182
+ if frozen?
183
+ super
184
+ else
185
+ super.tap do |copy|
186
+ copy.instance_variable_set(:@config, config.dup)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,185 @@
1
+ require 'concurrent/hash'
2
+
3
+ module Dry
4
+ module Configurable
5
+ # @private
6
+ class Config
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
18
+
19
+ # @private
20
+ def define_accessors!
21
+ @lock.synchronize do
22
+ break if config_defined?
23
+
24
+ settings.each do |setting|
25
+ next if setting.reserved?
26
+
27
+ define_method(setting.name) do
28
+ @config[setting.name]
29
+ end
30
+
31
+ define_method("#{setting.name}=") do |value|
32
+ raise FrozenConfig, 'Cannot modify frozen config' if frozen?
33
+ @config[setting.name] = setting.processor.(value)
34
+ end
35
+ end
36
+
37
+ @config_defined = true
38
+ end
39
+ end
40
+
41
+ # @private
42
+ def config_defined?
43
+ @config_defined
44
+ end
45
+ end
46
+
47
+ def initialize
48
+ @config = ::Concurrent::Hash.new
49
+ @lock = ::Mutex.new
50
+ @defined = false
51
+ end
52
+
53
+ def defined?
54
+ @defined
55
+ end
56
+
57
+ # @private
58
+ def define!(parent_config = EMPTY_HASH)
59
+ @lock.synchronize do
60
+ break if self.defined?
61
+
62
+ self.class.define_accessors!
63
+ set_values!(parent_config)
64
+
65
+ @defined = true
66
+ end
67
+
68
+ self
69
+ end
70
+
71
+ # @private
72
+ def finalize!
73
+ define!
74
+ @config.freeze
75
+ freeze
76
+ end
77
+
78
+ # Serialize config to a Hash
79
+ #
80
+ # @return [Hash]
81
+ #
82
+ # @api public
83
+ def to_h
84
+ @config.each_with_object({}) do |(key, value), hash|
85
+ case value
86
+ when Config
87
+ hash[key] = value.to_h
88
+ else
89
+ hash[key] = value
90
+ end
91
+ end
92
+ end
93
+ alias to_hash to_h
94
+
95
+ # Get config value by a key
96
+ #
97
+ # @param [String,Symbol] name
98
+ #
99
+ # @return Config value
100
+ def [](name)
101
+ setting = self.class.settings[name.to_sym]
102
+
103
+ if setting.nil?
104
+ raise_unknown_setting_error(name)
105
+ elsif setting.reserved?
106
+ @config[setting.name]
107
+ else
108
+ public_send(name)
109
+ end
110
+ end
111
+
112
+ # Set config value.
113
+ # Note that finalized configs cannot be changed.
114
+ #
115
+ # @param [String,Symbol] name
116
+ # @param [Object] value
117
+ def []=(name, value)
118
+ setting = self.class.settings[name.to_sym]
119
+
120
+ if setting.nil?
121
+ raise_unknown_setting_error(name)
122
+ elsif setting.reserved?
123
+ @config[setting.name] = setting.processor.(value)
124
+ else
125
+ public_send("#{name}=", value)
126
+ end
127
+ end
128
+
129
+ # Whether config has a key
130
+ #
131
+ # @param [Symbol] key
132
+ # @return [Bool]
133
+ def key?(name)
134
+ self.class.settings.name?(name)
135
+ end
136
+
137
+ # Recursively update values from a hash
138
+ #
139
+ # @param [Hash] values to set
140
+ # @return [Config]
141
+ def update(values)
142
+ values.each do |key, value|
143
+ if self[key].is_a?(Config)
144
+ self[key].update(value)
145
+ else
146
+ self[key] = value
147
+ end
148
+ end
149
+ self
150
+ end
151
+
152
+ def dup
153
+ if self.defined?
154
+ self.class.new.define!(to_h)
155
+ else
156
+ self.class.new
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ # @private
163
+ def set_values!(parent)
164
+ self.class.settings.each do |setting|
165
+ if parent.key?(setting.name) && !setting.node?
166
+ @config[setting.name] = parent[setting.name]
167
+ elsif setting.undefined?
168
+ @config[setting.name] = nil
169
+ elsif setting.node?
170
+ value = setting.value.create_config
171
+ value.define!(parent.fetch(setting.name, EMPTY_HASH))
172
+ self[setting.name] = value
173
+ else
174
+ self[setting.name] = setting.value
175
+ end
176
+ end
177
+ end
178
+
179
+ # @private
180
+ def raise_unknown_setting_error(name)
181
+ ::Kernel.raise ArgumentError, "+#{name}+ is not a setting name"
182
+ end
183
+ end
184
+ end
185
+ end