has_config 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +16 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +11 -1
- data/CHANGELOG.md +8 -1
- data/Gemfile +2 -0
- data/README.md +133 -37
- data/Rakefile +6 -7
- data/bin/console +3 -3
- data/gemfiles/4.0.gemfile +1 -0
- data/gemfiles/4.1.gemfile +1 -0
- data/gemfiles/4.2.gemfile +1 -0
- data/gemfiles/5.0.gemfile +6 -0
- data/has_config.gemspec +15 -12
- data/lib/has_config.rb +8 -90
- data/lib/has_config/active_record/model_adapter.rb +68 -0
- data/lib/has_config/active_record/processor.rb +60 -0
- data/lib/has_config/chain.rb +14 -0
- data/lib/has_config/configuration.rb +45 -0
- data/lib/has_config/engine.rb +27 -0
- data/lib/has_config/errors.rb +7 -0
- data/lib/has_config/value_parser.rb +15 -0
- data/lib/has_config/version.rb +1 -1
- metadata +27 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 467718d32a9ceae8d77b03f88c69871653fae7c3
|
4
|
+
data.tar.gz: 2e83f05c531b9e6b0acb65ae67304759bb0c32c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5af26b5b82655f08dd09cd21479e41da734e197289506a9de50fee7cc47232d192ecce3d8fe3e0a00590bc3059379b3589836d30bb326763a41d225c889a013f
|
7
|
+
data.tar.gz: 4f9ad634b7cd92dcf96c6bcabd13b718d98e6b4b6dfd82100bce8b32f96ec2dab25cc899a40d5c50101a6983673cf06a208e5bbd5a1845ae00aea6bd124abca1
|
data/.codeclimate.yml
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
@@ -3,12 +3,22 @@ rvm:
|
|
3
3
|
- 2.0
|
4
4
|
- 2.1
|
5
5
|
- 2.2
|
6
|
+
- 2.3.1
|
6
7
|
gemfile:
|
7
8
|
- gemfiles/4.0.gemfile
|
8
9
|
- gemfiles/4.1.gemfile
|
9
10
|
- gemfiles/4.2.gemfile
|
11
|
+
- gemfiles/5.0.gemfile
|
12
|
+
matrix:
|
13
|
+
exclude:
|
14
|
+
- rvm: 2.0
|
15
|
+
gemfile: gemfiles/5.0.gemfile
|
16
|
+
- rvm: 2.1
|
17
|
+
gemfile: gemfiles/5.0.gemfile
|
18
|
+
- rvm: 2.2
|
19
|
+
gemfile: gemfiles/5.0.gemfile
|
10
20
|
addons:
|
11
|
-
postgresql: '9.
|
21
|
+
postgresql: '9.4'
|
12
22
|
before_script:
|
13
23
|
- psql -c 'create database has_config_test;' -U postgres
|
14
24
|
script:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
-
## 0.
|
1
|
+
## 0.2.0 (2016-08-20)
|
2
|
+
|
3
|
+
* Full rewrite (Imported code from https://github.com/t27duck/cerulean)
|
4
|
+
* Allow predefined settings in a configuration file
|
5
|
+
* Allow chaining from models of the same setting
|
6
|
+
* Removed grouping functionality
|
7
|
+
|
8
|
+
## 0.1.0 (2015-06-20)
|
2
9
|
|
3
10
|
* Initial release
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
# HasConfig
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/t27duck/has_config.svg?branch=master)](https://travis-ci.org/t27duck/has_config)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/t27duck/has_config/badges/gpa.svg)](https://codeclimate.com/github/t27duck/has_config)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/t27duck/has_config/badges/coverage.svg)](https://codeclimate.com/github/t27duck/has_config/coverage)
|
4
6
|
|
5
|
-
|
7
|
+
When working with models in a large Rails project, you sometimes end up with "god objects" which start to be loaded down with several booleans, integers, and strings from select boxes that act as configuration options. As time goes on, you add more and more columns. As your database and user-base grows, adding even a single column more can bring your app to a hang during a deploy due to table locking or a slew of exceptions due to [issues and gotchas like this](https://github.com/rails/rails/issues/12330).
|
6
8
|
|
7
|
-
|
9
|
+
In an attempt to cut down on cluttering your model with boolean columns, `has_config` allows you to have a single column contain all configuration switches you could ever want. Adding another configuration option to a model no longer requires a migration to add a column. You can also continue writing code as if the model had all of those individual attributes.
|
8
10
|
|
9
|
-
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
Supported Rubies: 2.0, 2.1, 2.2, 2.3
|
14
|
+
|
15
|
+
Supported versions of ActiveRecord: 4.0, 4.1, 4.2, 5.0
|
10
16
|
|
11
17
|
## Installation
|
12
18
|
|
@@ -34,54 +40,47 @@ class AddConfigurationToClients < ActiveRecord::Migration
|
|
34
40
|
end
|
35
41
|
```
|
36
42
|
|
37
|
-
We now want to make that column a serialized hash in our model and include the `HasConfig` module.
|
43
|
+
We now want to make that column a serialized hash in our model and include the `HasConfig::ActiveReocrd::ModelAdapter` module.
|
38
44
|
|
39
45
|
```ruby
|
40
46
|
class Client < ActiveRecord::Base
|
41
47
|
serialize :configuration, Hash
|
42
|
-
include HasConfig
|
48
|
+
include HasConfig::ActiveRecord::ModelAdapter
|
43
49
|
end
|
44
50
|
```
|
45
51
|
|
46
|
-
If you are using PostgreSQL 9.2 or later, you can use the JSON data-type for the configuration column and not have to declare it as a serilaized attribute in the model as `ActiveRecord` will take care of that for you.
|
52
|
+
If you are using PostgreSQL 9.2 or later, you can use the JSON or JSONB (if using Rails 4.2 or later) data-type for the configuration column and not have to declare it as a serilaized attribute in the model as `ActiveRecord` will take care of that for you.
|
47
53
|
|
48
|
-
If you want to use a different column name, you may override the default by setting `self.
|
49
|
-
|
50
|
-
Finally, we're going to tell our model what configuration it'll hold. We do this via the `has_config` method the module provides. For now, here's a sensory overload example. We'll go into detail in the next part.
|
54
|
+
If you want to use a different column name, you may override the default by setting `self.has_config_configuration_column = 'other_column_name'` in the model.
|
51
55
|
|
52
56
|
```ruby
|
53
57
|
class Client < ActiveRecord::Base
|
54
58
|
serialize :configuration, Hash
|
55
|
-
include HasConfig
|
56
|
-
has_config :primary_color, :string, default: 'green'
|
57
|
-
has_config :secondary_color, :
|
58
|
-
has_config :rate_limit, :integer, validations: { numericality: { only_integer: true } }
|
59
|
-
has_config :category, :string, validations: { inclusion: { in: CATEGORIES } }
|
60
|
-
has_config :active, :boolean, default: false
|
59
|
+
include HasConfig::ActiveRecord::ModelAdapter
|
60
|
+
has_config :primary_color, config: { type: :string, default: 'green' }
|
61
|
+
has_config :secondary_color, config: { type: :string }
|
62
|
+
has_config :rate_limit, config: { type: :integer, validations: { numericality: { only_integer: true } } }
|
63
|
+
has_config :category, config: { type: :string, validations: { inclusion: { in: CATEGORIES } } }
|
64
|
+
has_config :active, config: { type: :boolean, default: false }
|
61
65
|
end
|
62
66
|
```
|
67
|
+
The `has_config` method is the primary interface for adding a setting to a model. The first argument is a symbol that represents the name of the setting.
|
63
68
|
|
64
|
-
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
has_config(key, type, default:nil, group:nil, validations:{})
|
68
|
-
```
|
69
|
-
|
70
|
-
At minimum, you must provide a `key` and `type`. The `key` is what you'll call this configuraiton item. The `type` can be either `:string`, `:integer`, or `:boolean`.
|
69
|
+
The `config` key is a hash that contains information describing your setting. The `type` is the only required key when including the `config` option.
|
71
70
|
|
72
|
-
|
71
|
+
`type` is the datatype of your setting. Valid options are `string`, `integer`, and `boolean`.
|
73
72
|
|
74
|
-
|
73
|
+
`default` is the value that will be used if the record does not have this setting set. If no `default` is provided, `nil` will be used.
|
75
74
|
|
76
|
-
|
75
|
+
`validations` allows the setting to use the standard ActiveRecord validations you'd use for any regular attribute.
|
77
76
|
|
78
77
|
Ok, still with me? Back to our example...
|
79
78
|
|
80
79
|
Here, the `Client` model has five configuration items on it: `primary_color`, `secondary_color`, `rate_limit`, `category`, and `active`. So, knowing what you just learned above...
|
81
80
|
|
82
|
-
`primary_color` is a string with a default value of green
|
81
|
+
`primary_color` is a string with a default value of "green".
|
83
82
|
|
84
|
-
`secondary_color` is a string without a default.
|
83
|
+
`secondary_color` is a string without a default.
|
85
84
|
|
86
85
|
`rate_limit` is an integer that validates its value is in fact, an integer.
|
87
86
|
|
@@ -115,21 +114,118 @@ client.errors.full_messages
|
|
115
114
|
|
116
115
|
Everything acts pretty much as you'd expect it too do. Configurations that fail validations make the record invalid. Passing in '1', 'true', `true`, etc casts boolean values. Passing in an empty string for an integer config casts as `nil`.
|
117
116
|
|
118
|
-
|
117
|
+
## Chaining with other models with the same setting
|
118
|
+
|
119
|
+
Let's say you have a `Client` model, a `Group` model, and a `User` model. A client has many groups and a group can have many users. A client can have configuration which globally affects all users; however, a group setting of the same name could override the global setting. HasConfig can handle this with relative ease.
|
120
|
+
|
121
|
+
First, let's set up the models
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class Client < ActiveRecord::Base
|
125
|
+
has_many :groups
|
126
|
+
# ...
|
127
|
+
has_config :some_setting, config: { type: :integer, default: 3 }
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
class Group < ActiveRecord::Base
|
132
|
+
belongs_to :client
|
133
|
+
has_many :users
|
134
|
+
|
135
|
+
# ...
|
136
|
+
has_config :some_setting, config: { type: :integer }, parent: :client
|
137
|
+
end
|
138
|
+
````
|
139
|
+
|
140
|
+
This introduces a new option for the `has_config` method: `parent`. The `parent` option specifies a method `HasConfig` can use to defer the setting value to another object.
|
141
|
+
|
142
|
+
Assume we have a client and a group stored in our database:
|
119
143
|
|
120
144
|
```irb
|
121
|
-
|
122
|
-
=>
|
145
|
+
g = Group.first
|
146
|
+
=> #<Group ...>
|
147
|
+
g.client
|
148
|
+
=> <#Client ...>
|
149
|
+
g.some_setting
|
150
|
+
=> nil
|
151
|
+
g.some_setting(:resolve)
|
152
|
+
=> 3
|
153
|
+
g.some_setting = 1
|
154
|
+
=> 1
|
155
|
+
g.some_setting(:resolve)
|
156
|
+
=> 1
|
157
|
+
g.some_setting = nil
|
158
|
+
=> nil
|
159
|
+
g.some_setting(:resolve)
|
160
|
+
=> 3
|
161
|
+
```
|
162
|
+
|
163
|
+
See what happened? Note the subtle change in how we reference the stting?
|
164
|
+
|
165
|
+
When we pass the symbol `:resolve` into the setting's getter method, and is blank, we will defer to the setting in the parent (in this case, `Client`) and use that value. If you do not pass `:resolve` in the getter, the local value will be used.
|
166
|
+
|
167
|
+
By default, `HasConfig` will go up the chain if the child model's value is `blank` (from `ActiveSupport`'s `blank?` method).
|
168
|
+
|
169
|
+
You can chain as deep as you want as long as the object returned from `parent` includes a setting of the same name as the child. Meaning, your `User` model can chain `some_setting` up to `group` which can chain up to `client`.
|
170
|
+
|
171
|
+
You do have some control over when `HasConfig` invokes the change via the `chain_on` option for the setting's config:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# Chain will be invoked if the local value is `nil`
|
175
|
+
has_config :setting1, config: { type: :string, chain_on: :nil }, parent: :some_method
|
176
|
+
|
177
|
+
# Chain will be invoked if the local value is `false`
|
178
|
+
has_config :setting2, config: { type: :string, chain_on: :false }, parent: :some_other_method
|
123
179
|
```
|
124
180
|
|
125
|
-
##
|
181
|
+
## Configuration file
|
182
|
+
|
183
|
+
An alternative to defining the definition of each setting in your model is to put them in a centralized configuration file.
|
184
|
+
|
185
|
+
Giving a file located at `#{Rails.root}/config/has_config.rb`:
|
126
186
|
|
127
|
-
|
187
|
+
```ruby
|
188
|
+
has_config :primary_color, config: { type: :string, default: 'green' }
|
189
|
+
has_config :secondary_color, config: { type: :string }
|
190
|
+
has_config :rate_limit, config: { type: :integer, validations: { numericality: { only_integer: true } } }
|
191
|
+
has_config :category, config: { :string, validations: { inclusion: { in: CATEGORIES } } }
|
192
|
+
has_config :active, config: { type: :boolean, default: false }
|
193
|
+
````
|
194
|
+
|
195
|
+
... and then somewhere in your app, call `HasConfig::Engine.load` (There's an optional `path:` argument to specify a different file path)
|
196
|
+
|
197
|
+
This will load up pre-configured setting information in your app. You can then just refer to each setting by name in your model:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
class Client < ActiveRecord::Base
|
201
|
+
serialize :configuration, Hash
|
202
|
+
include HasConfig::ActiveRecord::ModelAdapter
|
203
|
+
has_config :primary_color
|
204
|
+
has_config :secondary_color
|
205
|
+
has_config :rate_limit
|
206
|
+
has_config :category
|
207
|
+
has_config :active
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
You can also override the `default` and `validations` options for a pre-defined config:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
has_config :primary_color, config: { default: 'custom_value_unique_to_this_model' }
|
215
|
+
```
|
216
|
+
|
217
|
+
## Development
|
218
|
+
|
219
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
220
|
+
|
221
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
128
222
|
|
129
223
|
## Contributing
|
130
224
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
225
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/t27duck/has_config.
|
226
|
+
|
227
|
+
|
228
|
+
## License
|
229
|
+
|
230
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
231
|
+
|
data/Rakefile
CHANGED
@@ -1,20 +1,19 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
2
|
|
3
3
|
task default: :test
|
4
4
|
|
5
|
-
require
|
5
|
+
require 'rake/testtask'
|
6
6
|
Rake::TestTask.new do |t|
|
7
7
|
t.libs << 'test'
|
8
|
-
|
9
|
-
t.pattern = "test/test*.rb"
|
8
|
+
t.pattern = 'test/**/*_test.rb'
|
10
9
|
end
|
11
10
|
|
12
|
-
desc
|
11
|
+
desc 'Run a console with the environment loaded'
|
13
12
|
task :console do
|
14
13
|
require 'has_config'
|
15
14
|
|
16
|
-
require 'active_record'
|
17
|
-
require 'pg'
|
15
|
+
require 'active_record'
|
16
|
+
require 'pg'
|
18
17
|
require 'irb'
|
19
18
|
require 'irb/completion'
|
20
19
|
ARGV.clear
|
data/bin/console
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'has_config'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +10,5 @@ require "has_config"
|
|
10
10
|
# require "pry"
|
11
11
|
# Pry.start
|
12
12
|
|
13
|
-
require
|
13
|
+
require 'irb'
|
14
14
|
IRB.start
|
data/gemfiles/4.0.gemfile
CHANGED
data/gemfiles/4.1.gemfile
CHANGED
data/gemfiles/4.2.gemfile
CHANGED
data/has_config.gemspec
CHANGED
@@ -4,27 +4,30 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'has_config/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'has_config'
|
8
8
|
spec.version = HasConfig::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Tony Drake']
|
10
|
+
spec.email = ['t27duck@gmail.com']
|
11
11
|
|
12
|
-
spec.summary =
|
12
|
+
spec.summary = 'Quick record-specific configuration for your models'
|
13
13
|
spec.description = <<-DESC
|
14
14
|
Allows you to include and organize configuration options for each record in
|
15
15
|
a model without the need of complex joins to settings tables or constantly
|
16
16
|
adding random boolean and string columns
|
17
17
|
DESC
|
18
|
-
spec.homepage =
|
19
|
-
spec.license =
|
18
|
+
spec.homepage = 'http://github.com/t27duck/has_config'
|
19
|
+
spec.license = 'MIT'
|
20
20
|
|
21
21
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
-
spec.bindir =
|
22
|
+
spec.bindir = 'exe'
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
-
spec.require_paths = [
|
24
|
+
spec.require_paths = ['lib']
|
25
25
|
|
26
|
-
spec.
|
27
|
-
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
26
|
+
spec.required_ruby_version = '>= 2.0.0'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'activerecord', '>= 4.0'
|
31
|
+
spec.add_development_dependency 'activesupport', '>= 4.0'
|
32
|
+
spec.add_development_dependency 'pg'
|
30
33
|
end
|
data/lib/has_config.rb
CHANGED
@@ -1,93 +1,11 @@
|
|
1
|
-
require
|
1
|
+
require 'has_config/version'
|
2
|
+
require 'has_config/errors'
|
3
|
+
require 'has_config/configuration'
|
4
|
+
require 'has_config/value_parser'
|
5
|
+
require 'has_config/chain'
|
6
|
+
require 'has_config/engine'
|
7
|
+
require 'has_config/active_record/processor'
|
8
|
+
require 'has_config/active_record/model_adapter'
|
2
9
|
|
3
10
|
module HasConfig
|
4
|
-
def self.included(base)
|
5
|
-
base.extend ClassMethods
|
6
|
-
end
|
7
|
-
|
8
|
-
def configuration_for_group(group_name)
|
9
|
-
group_config = {}
|
10
|
-
(self.class.configuration_groups[group_name.to_s] || {}).each do |config_name|
|
11
|
-
group_config[config_name.to_sym] = public_send(config_name)
|
12
|
-
end
|
13
|
-
group_config
|
14
|
-
end
|
15
|
-
|
16
|
-
module ClassMethods
|
17
|
-
def has_config(key, type, default:nil, group:nil, validations:{})
|
18
|
-
raise ArgumentError, "Invalid type #{type}" unless [:string, :integer, :boolean].include?(type)
|
19
|
-
|
20
|
-
define_configuration_getter(key, default, type == :boolean)
|
21
|
-
define_configuration_setter(key, type)
|
22
|
-
set_configuration_group(key, group) if group.present?
|
23
|
-
set_configuration_validations(key, validations) if validations.present?
|
24
|
-
end
|
25
|
-
|
26
|
-
def configuration_groups
|
27
|
-
@configuration_groups ||= {}
|
28
|
-
end
|
29
|
-
|
30
|
-
def configuration_column
|
31
|
-
@configuration_column ||= 'configuration'
|
32
|
-
end
|
33
|
-
|
34
|
-
def configuration_column=(column_name)
|
35
|
-
@configuration_column = column_name.to_s
|
36
|
-
end
|
37
|
-
|
38
|
-
private ####################################################################
|
39
|
-
|
40
|
-
def define_configuration_getter(key, default, include_boolean=false)
|
41
|
-
define_method(key) do
|
42
|
-
config = (attributes[self.class.configuration_column] || {})
|
43
|
-
config[key.to_s].nil? ? default : config[key.to_s]
|
44
|
-
end
|
45
|
-
|
46
|
-
if include_boolean
|
47
|
-
define_method("#{key}?") do
|
48
|
-
config = (attributes[self.class.configuration_column] || {})
|
49
|
-
config[key.to_s].nil? ? default : config[key.to_s]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def define_configuration_setter(key, type)
|
55
|
-
define_method("#{key}=") do |input|
|
56
|
-
config = (attributes[self.class.configuration_column] || {})
|
57
|
-
original_value = config[key.to_s]
|
58
|
-
parsed_value = nil
|
59
|
-
|
60
|
-
if !input.nil?
|
61
|
-
parsed_value = case type
|
62
|
-
when :string
|
63
|
-
input.to_s
|
64
|
-
when :integer
|
65
|
-
input.present? ? input.to_i : nil
|
66
|
-
when :boolean
|
67
|
-
([true,1].include?(input) || input =~ (/(true|t|yes|y|1)$/i)) ? true : false
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
if original_value != parsed_value
|
72
|
-
config[key.to_s] = parsed_value
|
73
|
-
write_attribute(self.class.configuration_column, config)
|
74
|
-
public_send("#{self.class.configuration_column}_will_change!")
|
75
|
-
end
|
76
|
-
|
77
|
-
input
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def set_configuration_group(key, group)
|
82
|
-
@configuration_groups ||= {}
|
83
|
-
@configuration_groups[group.to_s] ||= []
|
84
|
-
@configuration_groups[group.to_s] << key.to_s
|
85
|
-
end
|
86
|
-
|
87
|
-
def set_configuration_validations(key, validation_config)
|
88
|
-
validates key, validation_config
|
89
|
-
end
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
11
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module HasConfig
|
2
|
+
module ActiveRecord
|
3
|
+
module ModelAdapter
|
4
|
+
DEFAULT_CONFIGURATION_COLUMN = 'configuration'.freeze
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def has_config_processor
|
11
|
+
@has_config_processor ||= HasConfig::ActiveRecord::Processor.new(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def has_config(key, parent: nil, config: {})
|
16
|
+
configuration = HasConfig::Engine.known_configurations[key.to_sym]
|
17
|
+
if config.present?
|
18
|
+
configuration = if configuration.nil?
|
19
|
+
HasConfig::Configuration.new(key.to_sym, config)
|
20
|
+
else
|
21
|
+
HasConfig::Configuration.modify(configuration, config)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
raise HasConfig::UnknownConfig, "Unknown config #{key}" if configuration.nil?
|
25
|
+
|
26
|
+
define_has_config_getter(configuration, parent: parent)
|
27
|
+
define_has_config_setter(configuration)
|
28
|
+
apply_has_config_validations(configuration)
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_config_configuration_column
|
32
|
+
@has_config_configuration_column ||= DEFAULT_CONFIGURATION_COLUMN
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_config_configuration_column=(column_name)
|
36
|
+
@has_config_configuration_column = column_name.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
private ################################################################
|
40
|
+
|
41
|
+
def define_has_config_getter(configuration, parent: nil)
|
42
|
+
define_method(configuration.name) do |mode = :none|
|
43
|
+
has_config_processor.fetch(configuration, parent: parent, mode: mode)
|
44
|
+
end
|
45
|
+
|
46
|
+
if configuration.type == :boolean
|
47
|
+
define_method("#{configuration.name}?") do |mode = :none|
|
48
|
+
public_send(configuration.name, mode)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_has_config_setter(configuration)
|
54
|
+
define_method("#{configuration.name}=") do |value|
|
55
|
+
has_config_processor.set(configuration, value)
|
56
|
+
value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_has_config_validations(configuration)
|
61
|
+
[configuration.validations].flatten.each do |validation|
|
62
|
+
validates configuration.name, validation
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module HasConfig
|
2
|
+
module ActiveRecord
|
3
|
+
class Processor
|
4
|
+
def initialize(model)
|
5
|
+
@model = model
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(configuration, parent: nil, mode: :none)
|
9
|
+
local = local_value(has_config_column_data, configuration)
|
10
|
+
|
11
|
+
if parent && mode == :resolve && HasConfig::Chain.invoke?(local, configuration.chain_on)
|
12
|
+
check_chain(configuration, parent)
|
13
|
+
parent_value = @model.public_send(parent).public_send(configuration.name, :resolve)
|
14
|
+
return parent_value unless parent_value.blank?
|
15
|
+
end
|
16
|
+
|
17
|
+
local
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(configuration, value)
|
21
|
+
data = has_config_column_data
|
22
|
+
parsed_value = HasConfig::ValueParser.parse(value, configuration.type)
|
23
|
+
|
24
|
+
if data[configuration.name] != parsed_value
|
25
|
+
data[configuration.name] = parsed_value
|
26
|
+
@model.send(:write_attribute, has_config_column, data)
|
27
|
+
@model.public_send("#{has_config_column}_will_change!")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private ##################################################################
|
32
|
+
|
33
|
+
def has_config_column
|
34
|
+
@model.class.has_config_configuration_column
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_config_column_data
|
38
|
+
@model.attributes[has_config_column] || {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def local_value(data, configuration)
|
42
|
+
if data[configuration.name].nil?
|
43
|
+
configuration.default
|
44
|
+
else
|
45
|
+
data[configuration.name]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_chain(configuration, parent)
|
50
|
+
unless @model.respond_to?(parent)
|
51
|
+
raise HasConfig::InvalidChain, "#{parent} is not available on this model"
|
52
|
+
end
|
53
|
+
|
54
|
+
unless @model.public_send(parent).respond_to?(configuration.name)
|
55
|
+
raise HasConfig::InvalidChain, "#{configuration.name} not available on #{parent}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module HasConfig
|
2
|
+
class Configuration
|
3
|
+
CHAINING_OPTIONS = %i(blank nil false).freeze
|
4
|
+
MODIFIABLE_ATTRS = %i(chain_on default validations).freeze
|
5
|
+
VALID_TYPES = %i(string integer boolean).freeze
|
6
|
+
|
7
|
+
attr_reader :name, :type
|
8
|
+
attr_accessor(*MODIFIABLE_ATTRS)
|
9
|
+
|
10
|
+
def self.modify(configuration, config)
|
11
|
+
configuration = configuration.dup
|
12
|
+
MODIFIABLE_ATTRS.each do |key|
|
13
|
+
configuration.public_send("#{key}=", config[key]) if config.key?(key)
|
14
|
+
end
|
15
|
+
configuration.validate
|
16
|
+
configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(name, type: nil, default: nil, validations: [], chain_on: :blank)
|
20
|
+
raise InvalidType, 'Type is required' if type.nil?
|
21
|
+
|
22
|
+
@chain_on = chain_on.to_sym
|
23
|
+
@default = default
|
24
|
+
@name = name.to_s
|
25
|
+
@type = type.to_sym
|
26
|
+
@validations = [validations].flatten
|
27
|
+
validate
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate
|
31
|
+
validate_configuration
|
32
|
+
validate_chain_on
|
33
|
+
end
|
34
|
+
|
35
|
+
private ####################################################################
|
36
|
+
|
37
|
+
def validate_configuration
|
38
|
+
raise InvalidType, "Invalid type #{type}" unless VALID_TYPES.include?(type)
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_chain_on
|
42
|
+
raise InvalidChainOption, "Invalid chainning option: #{chain_on}" unless CHAINING_OPTIONS.include?(chain_on)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module HasConfig
|
2
|
+
class Engine
|
3
|
+
class ConfigurationFileReader
|
4
|
+
def has_config(name, config: {})
|
5
|
+
Engine.register_configuration Configuration.new(name, config)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.known_configurations
|
10
|
+
@known_configurations ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load(path: 'config/has_config.rb')
|
14
|
+
raise ConfigurationFileNotFound, "No such file '#{path}'" unless File.exist?(path)
|
15
|
+
clear_configurations
|
16
|
+
ConfigurationFileReader.new.instance_eval(File.read(path))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.register_configuration(configuration)
|
20
|
+
known_configurations[configuration.name.to_sym] = configuration
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.clear_configurations
|
24
|
+
@known_configurations = {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HasConfig
|
2
|
+
class ValueParser
|
3
|
+
def self.parse(value, type)
|
4
|
+
return nil if value.nil?
|
5
|
+
case type
|
6
|
+
when :string
|
7
|
+
value.to_s
|
8
|
+
when :integer
|
9
|
+
value.present? ? value.to_i : nil
|
10
|
+
when :boolean
|
11
|
+
/\A(true|t|yes|y|1)\z/i === value.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/has_config/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_config
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Drake
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '4.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pg
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -76,7 +90,9 @@ executables: []
|
|
76
90
|
extensions: []
|
77
91
|
extra_rdoc_files: []
|
78
92
|
files:
|
93
|
+
- ".codeclimate.yml"
|
79
94
|
- ".gitignore"
|
95
|
+
- ".rubocop.yml"
|
80
96
|
- ".travis.yml"
|
81
97
|
- CHANGELOG.md
|
82
98
|
- CODE_OF_CONDUCT.md
|
@@ -89,8 +105,16 @@ files:
|
|
89
105
|
- gemfiles/4.0.gemfile
|
90
106
|
- gemfiles/4.1.gemfile
|
91
107
|
- gemfiles/4.2.gemfile
|
108
|
+
- gemfiles/5.0.gemfile
|
92
109
|
- has_config.gemspec
|
93
110
|
- lib/has_config.rb
|
111
|
+
- lib/has_config/active_record/model_adapter.rb
|
112
|
+
- lib/has_config/active_record/processor.rb
|
113
|
+
- lib/has_config/chain.rb
|
114
|
+
- lib/has_config/configuration.rb
|
115
|
+
- lib/has_config/engine.rb
|
116
|
+
- lib/has_config/errors.rb
|
117
|
+
- lib/has_config/value_parser.rb
|
94
118
|
- lib/has_config/version.rb
|
95
119
|
homepage: http://github.com/t27duck/has_config
|
96
120
|
licenses:
|
@@ -104,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
128
|
requirements:
|
105
129
|
- - ">="
|
106
130
|
- !ruby/object:Gem::Version
|
107
|
-
version:
|
131
|
+
version: 2.0.0
|
108
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
133
|
requirements:
|
110
134
|
- - ">="
|