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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0ff4a936f1e2bb34db9f906368df5cc8e1933f03
4
- data.tar.gz: f29d5a2372cc3ca6abecf6d4d0fc543467f94ca2
3
+ metadata.gz: 467718d32a9ceae8d77b03f88c69871653fae7c3
4
+ data.tar.gz: 2e83f05c531b9e6b0acb65ae67304759bb0c32c9
5
5
  SHA512:
6
- metadata.gz: 9154daeeedbc0ca628ab5e3a2ee87a6685eef73a492f35ebdefec323f13d69cfff7524c2845d203da3a77826ae3e7974c4be90765f93d031edd02afa9a994783
7
- data.tar.gz: 57b678e8f2e67a71139e884c236b9c0027189c6fe1fa83dc9f9abc4bca8bd23419e492e4fda12a94753112fbe418d148e42955ea94692822423882c05ea5478d
6
+ metadata.gz: 5af26b5b82655f08dd09cd21479e41da734e197289506a9de50fee7cc47232d192ecce3d8fe3e0a00590bc3059379b3589836d30bb326763a41d225c889a013f
7
+ data.tar.gz: 4f9ad634b7cd92dcf96c6bcabd13b718d98e6b4b6dfd82100bce8b32f96ec2dab25cc899a40d5c50101a6983673cf06a208e5bbd5a1845ae00aea6bd124abca1
@@ -0,0 +1,16 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ config:
6
+ languages:
7
+ - ruby
8
+ fixme:
9
+ enabled: true
10
+ rubocop:
11
+ enabled: true
12
+ ratings:
13
+ paths:
14
+ - "**.rb"
15
+ exclude_paths:
16
+ - test/
@@ -0,0 +1,18 @@
1
+ AllCops:
2
+ Exclude:
3
+ - test/**/*
4
+
5
+ Metrics/LineLength:
6
+ Max: 120
7
+
8
+ MethodLength:
9
+ Max: 20
10
+
11
+ Style/ClassAndModuleChildren:
12
+ Enabled: false
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ Style/PredicateName:
18
+ Enabled: false
@@ -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.3'
21
+ postgresql: '9.4'
12
22
  before_script:
13
23
  - psql -c 'create database has_config_test;' -U postgres
14
24
  script:
@@ -1,3 +1,10 @@
1
- ## 0.1.0 (6/20/2015)
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
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in has_config.gemspec
4
4
  gemspec
5
+
6
+ gem 'codeclimate-test-reporter', require: nil
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
- Tested against Ruby 2.0 - 2.2 with ActiveRecord 4.0 - 4.2
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
- 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 hault 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).
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
- 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 contiue writing code as if the model had all of those indiviual attributes.
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.configuration_column = 'other_column_name'` in the model.
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', group: :style
57
- has_config :secondary_color, :string, group: :style
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
- Let's look at the `has_config` signature first before we go any further:
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
- If a configuration item doesn't have a value, `nil` is returned by default. Or, you may provide your own default value with the `default` option.
71
+ `type` is the datatype of your setting. Valid options are `string`, `integer`, and `boolean`.
73
72
 
74
- If you have a series of configuraiton items are are related, you can organize them together with the `group` option.
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
- To the app, each configuraiton item is like a pseudo attribute on the model. Modle attributes can have validations. Use the `validations` option to pass in a hash of options you'd normally pass into the `validates` method for a regular attribute on the model.
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 and grouped in the "style" group of configuration options.
81
+ `primary_color` is a string with a default value of "green".
83
82
 
84
- `secondary_color` is a string without a default. It too is in the "style" group.
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
- Finally, you can access all configuration values under a specific group with the `configuration_for_group` method.
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
- client.configuration_for_group(:style)
122
- => {primary_color: 'green', secondary_color: nil}
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
- ## Testing
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
- Tests run using a PostgreSQL database called `has_config_test`. You should be able to just create a database named that and run `bundle exec rake test`.
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
- 1. Fork it ( https://github.com/[my-github-username]/has_config/fork )
132
- 2. Create your feature branch (`git checkout -b my-new-feature`)
133
- 3. Commit your changes (`git commit -am 'Add some feature'`)
134
- 4. Push to the branch (`git push origin my-new-feature`)
135
- 5. Create a new Pull Request
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 "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
2
 
3
3
  task default: :test
4
4
 
5
- require "rake/testtask"
5
+ require 'rake/testtask'
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs << 'test'
8
- #t.pattern = "test/**/*_test.rb"
9
- t.pattern = "test/test*.rb"
8
+ t.pattern = 'test/**/*_test.rb'
10
9
  end
11
10
 
12
- desc "Run a console with the environment loaded"
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "has_config"
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 "irb"
13
+ require 'irb'
14
14
  IRB.start
@@ -1,5 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gem "activerecord", "~> 4.0.0"
4
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
5
 
5
6
  gemspec :path=>"../"
@@ -1,5 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gem "activerecord", "~> 4.1.0"
4
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
5
 
5
6
  gemspec :path=>"../"
@@ -1,5 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gem "activerecord", "~> 4.2.0"
4
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
5
 
5
6
  gemspec :path=>"../"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.0.0"
4
+ gem "codeclimate-test-reporter", group: :test, require: nil
5
+
6
+ gemspec :path=>"../"
@@ -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 = "has_config"
7
+ spec.name = 'has_config'
8
8
  spec.version = HasConfig::VERSION
9
- spec.authors = ["Tony Drake"]
10
- spec.email = ["t27duck@gmail.com"]
9
+ spec.authors = ['Tony Drake']
10
+ spec.email = ['t27duck@gmail.com']
11
11
 
12
- spec.summary = %q{Quick record-specific configuration for your models}
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 = "http://github.com/t27duck/has_config"
19
- spec.license = "MIT"
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 = "exe"
22
+ spec.bindir = 'exe'
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
- spec.require_paths = ["lib"]
24
+ spec.require_paths = ['lib']
25
25
 
26
- spec.add_development_dependency "bundler"
27
- spec.add_development_dependency "rake", "~> 10.0"
28
- spec.add_development_dependency "activerecord", ">= 4.0"
29
- spec.add_development_dependency "pg"
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
@@ -1,93 +1,11 @@
1
- require "has_config/version"
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,14 @@
1
+ module HasConfig
2
+ class Chain
3
+ def self.invoke?(value, chain_on)
4
+ case chain_on
5
+ when :blank
6
+ value.blank?
7
+ when :nil
8
+ value.nil?
9
+ when :false
10
+ value == false
11
+ end
12
+ end
13
+ end
14
+ 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,7 @@
1
+ module HasConfig
2
+ ConfigurationFileNotFound = Class.new(StandardError)
3
+ InvalidChainOption = Class.new(StandardError)
4
+ InvalidChain = Class.new(StandardError)
5
+ InvalidType = Class.new(StandardError)
6
+ UnknownConfig = Class.new(StandardError)
7
+ 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
@@ -1,3 +1,3 @@
1
1
  module HasConfig
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'.freeze
3
3
  end
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.1.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-03-12 00:00:00.000000000 Z
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: '0'
131
+ version: 2.0.0
108
132
  required_rubygems_version: !ruby/object:Gem::Requirement
109
133
  requirements:
110
134
  - - ">="