dry-configurable 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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